From 4dc0f7c60b2d25590ce390e097c8ae9bd030b627 Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Sun, 8 Feb 2026 00:40:00 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha19=20-=20=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=20M4=20-=20=E6=94=AF=E6=8C=81=E6=8E=92?= =?UTF-8?q?=E5=BA=8F,=20=E6=90=9C=E7=B4=A2,=20=E6=91=98=E8=A6=81=E6=98=BE?= =?UTF-8?q?=E7=A4=BA,=20=E5=88=A0=E9=99=A4,=20=E6=B8=85=E7=A9=BA=E7=AD=89;?= =?UTF-8?q?=20=E4=BC=98=E5=8C=96=20UI=20=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_ar.json | 4 +- .changelog/lang_en.json | 4 +- .changelog/lang_es.json | 4 +- .changelog/lang_fr.json | 4 +- .changelog/lang_ja.json | 4 +- .changelog/lang_ko.json | 4 +- .changelog/lang_ru.json | 4 +- .changelog/lang_zh-Hans.json | 7 +- .changelog/lang_zh-Hant-HK.json | 4 +- .changelog/lang_zh-Hant-TW.json | 4 +- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 2 +- app/src/main/java/org/autojs/autojs/App.kt | 4 +- .../autojs/app/tool/JsonSocketClientTool.kt | 48 +- .../autojs/autojs/core/console/ConsoleImpl.kt | 6 +- .../plugin/center/PluginCenterActivity.kt | 2 +- .../plugin/center/PluginInfoDialogManager.kt | 12 +- .../plugin/center/PluginInstallActions.kt | 2 +- .../core/plugin/center/PluginInstaller.kt | 4 +- .../autojs/core/pref/PrefRxLongObserver.kt | 44 ++ .../core/record/GlobalActionRecorder.java | 7 +- .../autojs/core/ui/dialog/JsDialog.java | 14 +- .../tasker/TaskerScriptEditActivity.java | 2 +- .../autojs/autojs/network/UpdateChecker.java | 6 +- .../network/download/DownloadManager.java | 2 +- .../permission/AllFilesAccessPermission.kt | 2 +- .../DisplayOverOtherAppsPermission.kt | 2 +- .../permission/PostNotificationsPermission.kt | 2 +- .../autojs/pluginclient/DevPluginService.kt | 2 +- .../autojs/pluginclient/JsonSocketClient.kt | 18 +- .../autojs/autojs/rhino/AndroidClassLoader.kt | 4 +- .../history/HistoryCleanupScheduler.kt | 26 +- .../storage/history/HistoryCleanupWorker.kt | 107 ++-- .../autojs/storage/history/HistoryDao.kt | 105 +++- .../autojs/storage/history/HistoryDatabase.kt | 2 +- .../autojs/storage/history/HistoryEntities.kt | 10 +- .../autojs/storage/history/HistoryPrefs.kt | 78 +++ ...ositoryHandler.kt => HistoryRepository.kt} | 119 ++++- .../autojs/storage/history/TrashBlobStore.kt | 136 +++++ .../storage/history/TrashBlobStoreUtils.kt | 55 -- .../history/TrashPartialCleanupWorker.kt | 33 ++ .../autojs/storage/history/TrashRepository.kt | 370 ++++++++++++-- .../storage/history/TrashRestoreController.kt | 168 ++++++- .../history/VersionHistoryController.kt | 190 ++++++- .../autojs/theme/app/ColorHistoryDao.kt | 2 +- ...mAdapter.kt => ColorHistoryItemAdapter.kt} | 0 .../theme/app/ColorInfoDialogManager.kt | 6 +- .../autojs/theme/app/ColorItemAdapter.kt | 4 +- .../autojs/theme/app/ColorItemsActivity.kt | 4 +- .../theme/app/ColorLibrariesActivity.kt | 10 +- .../theme/app/ColorSelectBaseActivity.kt | 84 ++-- .../theme/app/ColorSettingRecyclerView.kt | 10 +- .../autojs/theme/app/PaletteHistoryDao.kt | 2 +- .../preference/MaterialListPreference.kt | 6 +- .../theme/preference/MaterialPreference.kt | 2 +- .../preference/ThemeColorSwitchPreference.kt | 2 +- .../timing/ExactAlarmPermissionHelper.kt | 4 +- .../autojs/ui/common/ScriptLoopDialog.java | 2 +- .../autojs/ui/common/ScriptOperations.java | 360 ++++--------- .../autojs/ui/error/ErrorDialogActivity.java | 4 +- .../autojs/autojs/ui/explorer/ExplorerView.kt | 78 ++- .../filechooser/FileChooserDialogBuilder.java | 2 +- .../autojs/ui/floating/CircularMenu.java | 2 +- .../ui/floating/CodeGenerateDialog.java | 2 +- .../autojs/ui/floating/LayoutFloatyWindow.kt | 2 +- .../autojs/ui/main/drawer/DrawerFragment.kt | 101 +++- .../main/drawer/DrawerMenuToggleableItem.kt | 2 +- .../ui/main/scripts/ApkInfoDialogManager.kt | 8 +- .../scripts/EditableFileInfoDialogManager.kt | 8 +- .../ui/main/scripts/MediaInfoDialogManager.kt | 6 +- .../autojs/ui/project/BuildActivity.java | 4 +- .../autojs/ui/settings/AboutActivity.kt | 8 +- ...ty.kt => DisplayReleaseHistoryActivity.kt} | 38 +- .../ui/settings/LauncherIconPreference.kt | 2 +- ...oryAdapter.kt => ReleaseHistoryAdapter.kt} | 42 +- ...onHistoryItem.kt => ReleaseHistoryItem.kt} | 2 +- ...ference.kt => ReleaseHistoryPreference.kt} | 4 +- ...ository.kt => ReleaseHistoryRepository.kt} | 17 +- .../ui/settings/WorkingDirectoryPreference.kt | 30 +- .../ui/storage/BaseSearchSortListActivity.kt | 305 +++++++++++ .../autojs/ui/storage/CommonSortMode.kt | 16 + .../storage/StorageUsageSummaryFormatter.kt | 84 ++++ .../autojs/autojs/ui/storage/TrashActivity.kt | 336 ++++++++----- .../autojs/autojs/ui/storage/TrashAdapter.kt | 34 ++ .../autojs/ui/storage/TrashViewHolder.kt | 57 +++ .../ui/storage/VersionHistoryActivity.kt | 474 +++++++++++++----- .../ui/storage/VersionHistoryFileAdapter.kt | 38 ++ .../storage/VersionHistoryFileViewHolder.kt | 123 +++++ .../autojs/{app => util}/DialogUtils.kt | 339 ++++++++++++- .../org/autojs/autojs/util/IntentUtils.kt | 2 +- .../autojs/autojs/util/MaterialDialogUtils.kt | 99 ---- .../org/autojs/autojs/util/StringUtils.kt | 10 +- .../org/autojs/autojs/util/UpdateUtils.kt | 5 +- .../java/org/autojs/autojs/util/ViewUtils.kt | 108 ++-- .../autojs/util/WorkingDirectoryUtils.kt | 14 +- ...ories.png => ic_about_release_history.png} | Bin .../ic_search_smaller_thicker_black_48dp.png | Bin 0 -> 3850 bytes app/src/main/res/drawable/ic_simple_file.png | Bin 0 -> 913 bytes .../main/res/drawable/ic_simple_folder.png | Bin 0 -> 2075 bytes .../main/res/drawable/ic_version_history.png | Bin 19887 -> 17173 bytes .../activity_about_function_buttons.xml | 20 +- app/src/main/res/layout/activity_build.xml | 3 +- ...l => activity_display_release_history.xml} | 0 .../res/layout/activity_project_config.xml | 3 +- app/src/main/res/layout/activity_trash.xml | 58 +++ .../res/layout/activity_version_history.xml | 58 +++ ...n_history.xml => item_release_history.xml} | 0 app/src/main/res/layout/item_trash.xml | 48 ++ app/src/main/res/menu/menu_color_items.xml | 6 +- .../main/res/menu/menu_color_libraries.xml | 6 +- ...s.xml => menu_display_release_history.xml} | 0 app/src/main/res/menu/menu_editor.xml | 6 +- app/src/main/res/menu/menu_main.xml | 7 +- app/src/main/res/menu/menu_trash.xml | 45 ++ .../main/res/menu/menu_version_history.xml | 43 ++ app/src/main/res/values-ar/strings.xml | 143 ++++-- app/src/main/res/values-en/strings.xml | 125 +++-- app/src/main/res/values-es/strings.xml | 139 +++-- app/src/main/res/values-fr/strings.xml | 139 +++-- app/src/main/res/values-ja/strings.xml | 143 ++++-- app/src/main/res/values-ko/strings.xml | 143 ++++-- app/src/main/res/values-ru/strings.xml | 153 ++++-- app/src/main/res/values-zh-rHK/strings.xml | 141 ++++-- app/src/main/res/values-zh-rTW/strings.xml | 141 ++++-- app/src/main/res/values-zh/strings.xml | 123 +++-- app/src/main/res/values/strings.xml | 143 ++++-- app/src/main/res/values/styles.xml | 4 +- app/src/main/res/xml/fragment_preferences.xml | 10 +- gradle/libs.versions.toml | 1 + .../colorpicker/ColorPickerDialog.java | 20 +- .../main/res/menu/cpv_dialog_more_options.xml | 4 +- .../src/main/res/values-ar/strings.xml | 2 +- .../src/main/res/values-en/strings.xml | 2 +- .../src/main/res/values-es/strings.xml | 2 +- .../src/main/res/values-fr/strings.xml | 2 +- .../src/main/res/values-ja/strings.xml | 2 +- .../src/main/res/values-ko/strings.xml | 2 +- .../src/main/res/values-ru/strings.xml | 2 +- .../src/main/res/values-zh-rHK/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 2 +- .../src/main/res/values-zh/strings.xml | 2 +- .../src/main/res/values/strings.xml | 2 +- version.properties | 6 +- 143 files changed, 4724 insertions(+), 1711 deletions(-) create mode 100644 app/src/main/java/org/autojs/autojs/core/pref/PrefRxLongObserver.kt create mode 100644 app/src/main/java/org/autojs/autojs/storage/history/HistoryPrefs.kt rename app/src/main/java/org/autojs/autojs/storage/history/{HistoryRepositoryHandler.kt => HistoryRepository.kt} (56%) create mode 100644 app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStore.kt delete mode 100644 app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt create mode 100644 app/src/main/java/org/autojs/autojs/storage/history/TrashPartialCleanupWorker.kt rename app/src/main/java/org/autojs/autojs/theme/app/{ThemeColorHistoriesItemAdapter.kt => ColorHistoryItemAdapter.kt} (100%) rename app/src/main/java/org/autojs/autojs/ui/settings/{DisplayVersionHistoriesActivity.kt => DisplayReleaseHistoryActivity.kt} (88%) rename app/src/main/java/org/autojs/autojs/ui/settings/{VersionHistoryAdapter.kt => ReleaseHistoryAdapter.kt} (85%) rename app/src/main/java/org/autojs/autojs/ui/settings/{VersionHistoryItem.kt => ReleaseHistoryItem.kt} (82%) rename app/src/main/java/org/autojs/autojs/ui/settings/{VersionHistoryPreference.kt => ReleaseHistoryPreference.kt} (82%) rename app/src/main/java/org/autojs/autojs/ui/settings/{VersionHistoryRepository.kt => ReleaseHistoryRepository.kt} (95%) create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/BaseSearchSortListActivity.kt create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/CommonSortMode.kt create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/StorageUsageSummaryFormatter.kt create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/TrashAdapter.kt create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/TrashViewHolder.kt create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileAdapter.kt create mode 100644 app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileViewHolder.kt rename app/src/main/java/org/autojs/autojs/{app => util}/DialogUtils.kt (59%) delete mode 100644 app/src/main/java/org/autojs/autojs/util/MaterialDialogUtils.kt rename app/src/main/res/drawable/{ic_about_version_histories.png => ic_about_release_history.png} (100%) create mode 100644 app/src/main/res/drawable/ic_search_smaller_thicker_black_48dp.png create mode 100644 app/src/main/res/drawable/ic_simple_file.png create mode 100644 app/src/main/res/drawable/ic_simple_folder.png rename app/src/main/res/layout/{activity_display_version_histories.xml => activity_display_release_history.xml} (100%) create mode 100644 app/src/main/res/layout/activity_trash.xml create mode 100644 app/src/main/res/layout/activity_version_history.xml rename app/src/main/res/layout/{item_version_history.xml => item_release_history.xml} (100%) create mode 100644 app/src/main/res/layout/item_trash.xml rename app/src/main/res/menu/{menu_display_version_histories.xml => menu_display_release_history.xml} (100%) create mode 100644 app/src/main/res/menu/menu_trash.xml create mode 100644 app/src/main/res/menu/menu_version_history.xml diff --git a/.changelog/lang_ar.json b/.changelog/lang_ar.json index d562d745..210e0daf 100644 --- a/.changelog/lang_ar.json +++ b/.changelog/lang_ar.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "ميزة سجل الإصدارات: عرض سجل التغييرات متعدد اللغات والإحصائيات", + "ميزة سجل الإصدار: عرض سجلات التغيير والإحصائيات متعددة اللغات", "الدالة timers.keepAlive (أصبحت عالمية) للحفاظ على بقاء السكربت نشطًا", "مستمعو الأحداث مثل engines.on('start/stop/error', callback) لأحداث المحرك العامة", "الدالة images.detectMultiColors للتحقق من ألوان متعددة النقاط _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "قد يتم حجب بعض محتوى الوثائق عبر الإنترنت في تبويب Docs وصفحة الوثائق بواسطة شريط التنقل", "في بعض الصفحات قد يؤدي النقر على أزرار شريط الأدوات إلى تفعيل حدث النقر على العنوان بالخطأ", - "السطور الفارغة في محرّر الشيفرة تظهر كرموز مربّعة على بعض الأجهزة", + "السطور الفارغة في محرّر الشيفرة تظهر كرموز مربّعة على بعض الأجهزة (حل مؤقت)", "مربع حوار لوحة الألوان في إعداد لون السمة قد يتراكم بلا حدود", "زر رفع الصوت لا يوقف جميع السكربتات عند تعطيل خدمة الوصول", "تداخل لوحة المفاتيح عند تحرير بث مخصّص في صفحة المهام المجدولة", diff --git a/.changelog/lang_en.json b/.changelog/lang_en.json index 35d2a1dc..efeb5e87 100644 --- a/.changelog/lang_en.json +++ b/.changelog/lang_en.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "Version history feature: view multi-language release changelogs and statistics", + "Release history feature: view multi-language release changelogs and statistics", "timers.keepAlive method (now global) for keeping the script alive", "Event listeners such as engines.on('start/stop/error', callback) for global engine events", "images.detectMultiColors method for multi-point color verification _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "Online docs in the Home > Docs tab and Docs activity may be covered by the system navigation bar", "Clicking Toolbar buttons on some pages could accidentally trigger the title click event", - "Blank lines in the code editor showed box glyphs on some devices", + "Blank lines in the code editor showed box glyphs on some devices (tentative fix)", "Color-picker dialog in the theme-color settings page could stack infinitely", "Volume-up key failed to stop all scripts when the accessibility service was disabled", "IME overlay issue when editing custom broadcast content in the Scheduled Tasks page", diff --git a/.changelog/lang_es.json b/.changelog/lang_es.json index f471c7af..70f9e8b7 100644 --- a/.changelog/lang_es.json +++ b/.changelog/lang_es.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "Función de historial de versiones: ver registros de cambios multilenguaje y estadísticas", + "Función de historial de lanzamientos: ver registros de cambios y estadísticas de lanzamientos en múltiples idiomas", "Método timers.keepAlive (ahora global) para mantener activo el script", "Métodos de escucha de eventos como engines.on('start/stop/error', callback) para eventos globales del motor", "Método images.detectMultiColors para verificación de colores multipunto _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "El contenido de la documentación en línea en la pestaña Documentos y la actividad Documentos podía quedar cubierto por la barra de navegación del sistema", "En algunas páginas, al pulsar botones de la Toolbar se podía activar por error el evento de clic en el título", - "Las líneas en blanco del editor de código mostraban caracteres de cuadro en ciertos dispositivos", + "Las líneas en blanco del editor de código mostraban caracteres de cuadro en ciertos dispositivos (solución tentativa)", "El cuadro de diálogo del selector de colores en los ajustes de color de tema podía apilarse infinitamente", "La tecla de subir volumen no detenía todos los scripts cuando el servicio de accesibilidad estaba desactivado", "Superposición del teclado IME al editar contenido de difusión personalizado en la página de tareas programadas", diff --git a/.changelog/lang_fr.json b/.changelog/lang_fr.json index 0f7aae20..d974ce67 100644 --- a/.changelog/lang_fr.json +++ b/.changelog/lang_fr.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "Fonction historique des versions : consulter les journaux de mise à jour multilingues et les statistiques", + "Fonctionnalité d'historique des versions : visualisation des changelogs de versions multilingues et des statistiques", "Méthode timers.keepAlive (désormais globale) pour maintenir le script actif", "Méthodes d'écoute d'événements telles que engines.on('start/stop/error', callback) pour les événements globaux du moteur", "Méthode images.detectMultiColors pour la vérification de couleurs multipoints _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "Le contenu de la documentation en ligne dans l'onglet Docs et l'activité Docs pouvait être recouvert par la barre de navigation système", "Sur certaines pages, cliquer sur les boutons de la Toolbar pouvait déclencher par erreur l'événement de clic sur le titre", - "Les lignes vides dans l'éditeur de code affichaient des carrés sur certains appareils", + "Les lignes vides dans l'éditeur de code affichaient des carrés sur certains appareils (solution provisoire)", "La boîte de dialogue du sélecteur de couleurs dans les paramètres de couleur du thème pouvait se superposer indéfiniment", "La touche volume + ne stoppait pas tous les scripts lorsque le service d'accessibilité était désactivé", "Chevauchement du clavier IME lors de l'édition d'un contenu de diffusion personnalisé dans la page des tâches planifiées", diff --git a/.changelog/lang_ja.json b/.changelog/lang_ja.json index 290404dc..437e07f7 100644 --- a/.changelog/lang_ja.json +++ b/.changelog/lang_ja.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "バージョン履歴機能: 多言語の変更履歴と統計を閲覧", + "リリース履歴機能: 多言語リリース変更履歴と統計の表示", "timers.keepAlive メソッド (グローバルへ昇格) によりスクリプトの活動を維持", "engines.on('start/stop/error', callback) などのイベントリスナーでエンジンのグローバルイベントを受信", "images.detectMultiColors メソッドで複数点のカラーを検査 _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "Docs タブおよび Docs アクティビティのオンラインドキュメントがシステムナビゲーションバーと重なる場合がある問題を修正", "一部ページで Toolbar ボタンをタップすると誤ってタイトルのクリックイベントが発火する問題を修正", - "コードエディタの空行が一部デバイスで四角形として表示される問題を修正", + "コードエディタの空行が一部デバイスで四角形として表示される問題を修正 (暫定対応)", "テーマカラー設定のカラーピッカーダイアログが無限に重なる問題を修正", "アクセシビリティサービスが無効のときに音量アップキーが全スクリプトを停止しない問題を修正", "スケジュールタスクでユーザー定義ブロードキャストメッセージを編集する際に IME が入力エリアを覆う問題を修正", diff --git a/.changelog/lang_ko.json b/.changelog/lang_ko.json index 666819ad..9fbdb532 100644 --- a/.changelog/lang_ko.json +++ b/.changelog/lang_ko.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "버전 히스토리 기능: 다국어 변경 로그 및 통계 보기", + "릴리스 히스토리 기능: 다국어 릴리스 체인지로그 및 통계 보기", "timers.keepAlive 메서드 (전역으로 승격) 로 스크립트 활성 유지", "engines.on('start/stop/error', callback) 등의 이벤트 리스너로 엔진 전역 이벤트 수신", "images.detectMultiColors 메서드로 여러 지점 색상 검사 _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "Docs 탭 및 Docs 액티비티의 온라인 문서가 시스템 네비게이션 바에 가려질 수 있는 문제 수정", "일부 페이지에서 Toolbar 버튼을 누르면 제목 클릭 이벤트가 잘못 발생하는 문제 수정", - "코드 편집기 빈 줄이 일부 기기에서 사각형으로 보이는 문제 수정", + "일부 기기에서 코드 편집기 빈 줄이 사각형 문자로 표시되는 문제 (수정 시도)", "테마 색상 설정의 컬러 피커 대화상자가 무한 중첩되는 문제 수정", "접근성 서비스가 꺼진 상태에서 볼륨 업 키가 모든 스크립트를 중지하지 않는 문제 수정", "예약 작업에서 사용자 정의 브로드캐스트 메시지를 편집할 때 IME가 입력 영역을 덮는 문제 수정", diff --git a/.changelog/lang_ru.json b/.changelog/lang_ru.json index 04e1733a..5172c93d 100644 --- a/.changelog/lang_ru.json +++ b/.changelog/lang_ru.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "Функция истории версий: просмотр многоязычных журналов изменений и статистики", + "Функция истории релизов: просмотр многоязычных журналов изменений релизов и статистики", "Метод timers.keepAlive (теперь глобальный) для поддержания активности скрипта", "Слушатели событий, такие как engines.on('start/stop/error', callback), для глобальных событий движка", "Метод images.detectMultiColors для проверки нескольких точек цвета _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "Онлайн-документация на вкладке Docs и в активности Docs могла перекрываться системной навигационной панелью", "На некоторых страницах нажатие кнопок Toolbar могло ошибочно вызывать событие клика по заголовку", - "Пустые строки в редакторе кода отображались квадратиками на ряде устройств", + "Пустые строки в редакторе кода отображались квадратиками на ряде устройств (предварительное решение)", "Диалог выбора цвета в настройках темы мог бесконечно накладываться друг на друга", "Кнопка увеличения громкости не останавливала все скрипты при выключенной службе доступности", "IME перекрывал ввод при редактировании пользовательского широковещательного сообщения в расписании задач", diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 16528145..fdcea617 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -93,7 +93,7 @@ "输入事件观察器 InputEventObserver 可能导致应用启动时明显卡顿的问题", "Shizuku 用户服务进程未能正常结束导致进程堆积的问题 _[`issue #474`](http://issues.autojs6.com/474)_", "打包应用无法正常使用 Paddle OCR 与 Rapid OCR 功能的问题", - "版本历史页面部分系统因字体差别导致统计数据显示不完整的问题", + "发行历史页面部分系统因字体差别导致统计数据显示不完整的问题", "部分设备无法正常初始化 MLKit Google OCR 的问题 (试修) _[`issue #8`](http://issues.autojs6.com/8#issuecomment-3117061768)_", "部分设备无法正常触发文件管理器功能按钮点击事件的问题 (试修) _[`issue #465`](http://issues.autojs6.com/465)_", "ErrorDialogActivity 可能无法正常启动或短时间自动消失的问题 _[`issue #479`](http://issues.autojs6.com/479)_ _[`issue #471`](http://issues.autojs6.com/471)_ _[`issue #414`](http://issues.autojs6.com/414)_ _[`issue #340`](http://issues.autojs6.com/340#issuecomment-2973485826)_", @@ -169,6 +169,7 @@ "附加 AndroidX Activity (KTX) 版本 1.12.2", "附加 AndroidX Core (KTX) 版本 1.15.0", "附加 AndroidX Work Runtime 版本 2.11.0", + "附加 Room RxJava2 版本 2.8.1", "升级 Android SDK 版本 35 -> 36", "升级 Gradle 版本 8.14 -> 8.14.3", "升级 Rhino 版本 1.8.1-SNAPSHOT -> 2.0.0-SNAPSHOT", @@ -215,7 +216,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "版本历史功能, 可查看发行版本历史更新记录 (多语言) 与统计数据", + "发行历史功能, 可查看发行版本的发行历史记录 (多语言) 与统计数据", "timers.keepAlive 方法 (已全局化), 用于保持脚本活跃状态", "engines.on('start/stop/error', callback) 等事件监听方法, 用于监听脚本引擎全局事件", "images.detectMultiColors 方法, 用于多点颜色校验 _[`issue #374`](http://issues.autojs6.com/374)_", @@ -230,7 +231,7 @@ "fix": [ "主页文档标签及文档活动页面显示在线文档时部分内容被系统导航栏遮挡的问题", "部分页面 Toolbar 点击按钮时可能导致标题点击事件误触发的问题", - "部分设备代码编辑器空行显示方框字符的问题", + "部分设备代码编辑器空行显示方框字符的问题 (试修)", "主题色设置页面调色盘对话框可能无限叠加的问题", "无障碍服务关闭时音量加键停止所有脚本功能失效的问题", "定时任务页面编辑自定义广播内容时出现的输入法遮挡问题", diff --git a/.changelog/lang_zh-Hant-HK.json b/.changelog/lang_zh-Hant-HK.json index 0649f734..99e48d61 100644 --- a/.changelog/lang_zh-Hant-HK.json +++ b/.changelog/lang_zh-Hant-HK.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "版本歷史功能, 可查看發行版本歷史更新記錄 (多語言) 與統計數據", + "發行歷史功能, 可查看發行版本的發行歷史記錄 (多語言) 與統計數據", "timers.keepAlive 方法 (已全局化), 用於保持腳本活躍狀態", "engines.on('start/stop/error', callback) 等事件監聽方法, 用於監聽腳本引擎全局事件", "images.detectMultiColors 方法, 用於多點顏色校驗 _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "主頁文檔標籤及文檔活動頁面顯示在線文檔時部分內容被系統導航欄遮擋的問題", "部分頁面 Toolbar 點擊按鈕時可能導致標題點擊事件誤觸發的問題", - "部分設備代碼編輯器空行顯示方框字符的問題", + "部分設備代碼編輯器空行顯示方框字符的問題 (試修)", "主題色設置頁面調色盤對話框可能無限疊加的問題", "無障礙服務關閉時音量加鍵停止所有腳本功能失效的問題", "定時任務頁面編輯自定義廣播內容時出現的輸入法遮擋問題", diff --git a/.changelog/lang_zh-Hant-TW.json b/.changelog/lang_zh-Hant-TW.json index c12e4164..6b379ac3 100644 --- a/.changelog/lang_zh-Hant-TW.json +++ b/.changelog/lang_zh-Hant-TW.json @@ -25,7 +25,7 @@ "v6.6.3": { "released_date": "2025/05/27", "feature": [ - "版本歷史功能, 可檢視發行版本歷史更新記錄 (多語言) 與統計資料", + "發行歷史功能, 可檢視發行版本的發行歷史記錄 (多語言) 與統計資料", "timers.keepAlive 方法 (已全域性化), 用於保持指令碼活躍狀態", "engines.on('start/stop/error', callback) 等事件監聽方法, 用於監聽指令碼引擎全域性事件", "images.detectMultiColors 方法, 用於多點顏色校驗 _[`issue #374`](http://issues.autojs6.com/374)_", @@ -40,7 +40,7 @@ "fix": [ "主頁文件標籤及文件活動頁面顯示線上文件時部分內容被系統導航欄遮擋的問題", "部分頁面 Toolbar 點選按鈕時可能導致標題點選事件誤觸發的問題", - "部分裝置程式碼編輯器空行顯示方框字元的問題", + "部分裝置程式碼編輯器空行顯示方框字元的問題 (試修)", "主題色設定頁面調色盤對話方塊可能無限疊加的問題", "無障礙服務關閉時音量加鍵停止所有指令碼功能失效的問題", "定時任務頁面編輯自定義廣播內容時出現的輸入法遮擋問題", diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 182d8f51..eb4d8f64 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -229,6 +229,7 @@ dependencies /* Unclassified */ { // Room implementation(libs.room.runtime) implementation(libs.room.ktx) + implementation(libs.room.rxjava2) ksp(libs.room.compiler) // ApkSig diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48286556..dcff625b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -278,7 +278,7 @@ android:theme="@style/AppTheme.Settings" /> MaterialDialog.Builder(context) - .title(R.string.text_histories) - .content(R.string.text_no_histories) - .items(JsonSocketClient.serverAddressHistories) - .itemsCallback { dHistories, _, _, text -> - dHistories.dismiss() + .title(R.string.text_history) + .content(R.string.text_no_history) + .items(JsonSocketClient.serverAddressHistory) + .itemsCallback { dHistory, _, _, text -> + dHistory.dismiss() dialog.inputEditText?.setText(text) validateAndConnectToRemoteServer(dialog) } .choiceWidgetThemeColor() - .itemsLongCallback { dHistories, _, _, text -> + .itemsLongCallback { dHistory, _, _, text -> MaterialDialog.Builder(context) .title(R.string.text_prompt) .content(R.string.text_confirm_to_delete) @@ -241,12 +241,12 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { .positiveColorRes(R.color.dialog_button_caution) .onPositive { ds, _ -> ds.dismiss() - JsonSocketClient.removeFromHistories(text.toString()) - dHistories.items?.let { + JsonSocketClient.removeFromHistory(text.toString()) + dHistory.items?.let { it.remove(text) - dHistories.notifyItemsChanged() - DialogUtils.toggleContentViewByItems(dHistories) - DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL) + dHistory.notifyItemsChanged() + DialogUtils.toggleContentViewByItems(dHistory) + DialogUtils.toggleActionButtonAbilityByItems(dHistory, DialogAction.NEUTRAL) } } .show() @@ -254,29 +254,29 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { } .neutralText(R.string.dialog_button_clear_items) .neutralColorRes(R.color.dialog_button_warn) - .onNeutral { dHistories, _ -> + .onNeutral { dHistory, _ -> MaterialDialog.Builder(context) .title(R.string.text_prompt) - .content(R.string.text_confirm_to_clear_all_histories) + .content(R.string.text_confirm_to_clear_the_history) .negativeText(R.string.dialog_button_cancel) .negativeColorRes(R.color.dialog_button_default) .positiveText(R.string.dialog_button_confirm) .positiveColorRes(R.color.dialog_button_caution) .onPositive { _, _ -> - JsonSocketClient.clearAllHistories() - dHistories.items?.let { + JsonSocketClient.clearHistory() + dHistory.items?.let { it.clear() - dHistories.notifyItemsChanged() - DialogUtils.toggleContentViewByItems(dHistories) - DialogUtils.toggleActionButtonAbilityByItems(dHistories, DialogAction.NEUTRAL) + dHistory.notifyItemsChanged() + DialogUtils.toggleContentViewByItems(dHistory) + DialogUtils.toggleActionButtonAbilityByItems(dHistory, DialogAction.NEUTRAL) } - ViewUtils.showSnack(dHistories.view, R.string.text_all_histories_cleared) + ViewUtils.showSnack(dHistory.view, R.string.text_history_has_been_cleared) } .show() } .positiveText(R.string.dialog_button_back) .positiveColorRes(R.color.dialog_button_default) - .onPositive { dHistories, _ -> dHistories.dismiss() } + .onPositive { dHistory, _ -> dHistory.dismiss() } .autoDismiss(false) .show() .also { @@ -552,7 +552,7 @@ class JsonSocketClientTool(context: Context) : AbstractJsonSocketTool(context) { return } - val isInHistory = { JsonSocketClient.serverAddressHistories.contains(input) } + val isInHistory = { JsonSocketClient.serverAddressHistory.contains(input) } val isPotentiallyInvalid = { POTENTIALLY_INVALID_IP_ADRESS_LIST_FOR_REMOTE_SERVER.any { input.matches(it) } } when { diff --git a/app/src/main/java/org/autojs/autojs/core/console/ConsoleImpl.kt b/app/src/main/java/org/autojs/autojs/core/console/ConsoleImpl.kt index 1f8aeedf..28641671 100644 --- a/app/src/main/java/org/autojs/autojs/core/console/ConsoleImpl.kt +++ b/app/src/main/java/org/autojs/autojs/core/console/ConsoleImpl.kt @@ -26,7 +26,7 @@ import org.autojs.autojs.annotation.OmniColor import org.autojs.autojs.annotation.ScriptInterface import org.autojs.autojs.core.pref.Language import org.autojs.autojs.core.ui.inflater.util.Gravities -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.permission.DisplayOverOtherAppsPermission import org.autojs.autojs.runtime.ScriptRuntime import org.autojs.autojs.runtime.api.AbstractConsole @@ -191,7 +191,7 @@ open class ConsoleImpl(val uiHandler: UiHandler) : AbstractConsole() { } catch (e: Exception) { e.printStackTrace() if (text.length < mSafeSizeToCopy || cutOutEntriesSize >= 0) { - ViewUtils.showToast(context, R.string.text_failed_to_copy) + ViewUtils.showToast(context, R.string.error_failed_to_copy) return } try { @@ -199,7 +199,7 @@ open class ConsoleImpl(val uiHandler: UiHandler) : AbstractConsole() { copyAll(cutOutEntries.joinToString("\n") { it.content }, cutOutEntries.size) } catch (e: Exception) { e.printStackTrace() - ViewUtils.showToast(context, R.string.text_failed_to_copy) + ViewUtils.showToast(context, R.string.error_failed_to_copy) } } } diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt index 99800c03..30ac43d4 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginCenterActivity.kt @@ -14,7 +14,7 @@ import kotlinx.coroutines.launch import org.autojs.autojs.ui.BaseActivity import org.autojs.autojs.ui.widget.SearchViewItem import org.autojs.autojs.util.IntentUtils.startSafely -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.ViewUtils.onceGlobalLayout import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt index 795c06ee..7d88deed 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInfoDialogManager.kt @@ -17,10 +17,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.autojs.autojs.app.DialogUtils.showAdaptive -import org.autojs.autojs.util.MaterialDialogUtils.makeSettingsLaunchable -import org.autojs.autojs.util.MaterialDialogUtils.makeTextCopyable -import org.autojs.autojs.util.MaterialDialogUtils.setCopyableTextIfAbsent +import org.autojs.autojs.util.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.makeSettingsLaunchable +import org.autojs.autojs.util.DialogUtils.makeTextCopyable +import org.autojs.autojs.util.DialogUtils.setCopyableTextIfAbsent import org.autojs.autojs.runtime.api.augment.converter.core.Bytes import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.util.ColorUtils @@ -223,10 +223,10 @@ object PluginInfoDialogManager { .title(info.version ?: info.title) .options(listOf(ignoreUpdateOption)) .content(R.string.text_retrieving_release_notes) - .neutralText(R.string.dialog_button_version_histories) + .neutralText(R.string.dialog_button_release_history) .neutralColor(context.getColor(R.color.dialog_button_hint)) .onNeutral { _, _ -> - // DisplayVersionHistoriesActivity.launch(context) + // DisplayReleaseHistoryActivity.launch(context) } .negativeText(R.string.dialog_button_cancel) .negativeColor(context.getColor(R.color.dialog_button_default)) diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt index f3200caa..795dade6 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstallActions.kt @@ -5,7 +5,7 @@ import androidx.lifecycle.LifecycleCoroutineScope import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import kotlinx.coroutines.launch -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs6.R object PluginInstallActions { diff --git a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt index 2e1ed063..b20f478e 100644 --- a/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt +++ b/app/src/main/java/org/autojs/autojs/core/plugin/center/PluginInstaller.kt @@ -10,8 +10,8 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext -import org.autojs.autojs.app.DialogUtils.applyProgressThemeColorTintLists -import org.autojs.autojs.app.DialogUtils.setProgressNumberFormatByBytes +import org.autojs.autojs.util.DialogUtils.applyProgressThemeColorTintLists +import org.autojs.autojs.util.DialogUtils.setProgressNumberFormatByBytes import org.autojs.autojs.runtime.api.Mime import org.autojs.autojs.ui.main.scripts.ApkInfoDialogManager import org.autojs.autojs.util.ClipboardUtils diff --git a/app/src/main/java/org/autojs/autojs/core/pref/PrefRxLongObserver.kt b/app/src/main/java/org/autojs/autojs/core/pref/PrefRxLongObserver.kt new file mode 100644 index 00000000..a21db68a --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/pref/PrefRxLongObserver.kt @@ -0,0 +1,44 @@ +package org.autojs.autojs.core.pref + +import android.content.SharedPreferences +import io.reactivex.BackpressureStrategy +import io.reactivex.Flowable +import org.autojs.autojs.util.StringUtils.key + +/** + * Rx wrappers for Pref. + * zh-CN: Pref 的 Rx 包装. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 5, 2026. + */ +object PrefRx { + + /** + * Observe a Long preference value. + * zh-CN: 监听一个 Long 类型的偏好值. + */ + fun observeLong(keyRes: Int, defaultValue: Long): Flowable { + val sp = Pref.get() + val k = key(keyRes) + + return Flowable.create({ emitter -> + val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey -> + if (changedKey == k && !emitter.isCancelled) { + emitter.onNext(sp.getLong(k, defaultValue)) + } + } + + // Emit current value immediately. + // zh-CN: 立即发送当前值. + emitter.onNext(sp.getLong(k, defaultValue)) + + sp.registerOnSharedPreferenceChangeListener(listener) + + // Unregister on dispose. + // zh-CN: 取消订阅时反注册监听器. + emitter.setCancellable { + sp.unregisterOnSharedPreferenceChangeListener(listener) + } + }, BackpressureStrategy.LATEST).distinctUntilChanged() + } +} diff --git a/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java b/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java index 433294df..a4432bf9 100644 --- a/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java +++ b/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java @@ -3,14 +3,13 @@ package org.autojs.autojs.core.record; import android.content.Context; import android.view.ContextThemeWrapper; import com.afollestad.materialdialogs.MaterialDialog; -import org.autojs.autojs.app.DialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.app.GlobalAppContext; import org.autojs.autojs.core.pref.Pref; import org.autojs.autojs.core.record.inputevent.InputEventRecorder; import org.autojs.autojs.core.record.inputevent.InputEventToAutoFileRecorder; import org.autojs.autojs.core.record.inputevent.InputEventToRootAutomatorRecorder; import org.autojs.autojs.core.record.inputevent.TouchRecorder; -import org.autojs.autojs.util.MaterialDialogUtils; import org.autojs.autojs.ui.common.ScriptOperations; import org.autojs.autojs.util.ClipboardUtils; import org.autojs.autojs.util.ViewUtils; @@ -183,8 +182,8 @@ public class GlobalActionRecorder implements Recorder.OnStateChangedListener { .positiveText(R.string.dialog_button_confirm) .positiveColorRes(R.color.dialog_button_attraction) .canceledOnTouchOutside(false); - MaterialDialogUtils.choiceWidgetThemeColor(builder); - DialogUtils.showAdaptive(builder.build()); + DialogUtils.choiceWidgetThemeColor(builder); + DialogUtils.buildAndShowAdaptive(builder::build); } private String getString(int res) { diff --git a/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialog.java b/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialog.java index 705fd092..32c008c8 100644 --- a/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialog.java +++ b/app/src/main/java/org/autojs/autojs/core/ui/dialog/JsDialog.java @@ -3,6 +3,7 @@ package org.autojs.autojs.core.ui.dialog; import android.app.ActionBar; import android.app.Activity; import android.content.Context; +import android.content.ContextWrapper; import android.content.DialogInterface; import android.graphics.Typeface; import android.graphics.drawable.Drawable; @@ -36,7 +37,6 @@ import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.internal.MDButton; import com.google.android.material.snackbar.Snackbar; -import org.autojs.autojs.app.DialogUtils; import org.autojs.autojs.core.eventloop.EventEmitter; import org.autojs.autojs.tool.UiHandler; import org.autojs.autojs6.R; @@ -81,7 +81,7 @@ public class JsDialog { @SuppressWarnings("deprecation") private void checkWindowType() { Context context = mDialog.getContext(); - if (!DialogUtils.isActivityContext(context)) { + if (!isActivityContext(context)) { Window window = mDialog.getWindow(); if (window != null) { int type; @@ -95,6 +95,16 @@ public class JsDialog { } } + private boolean isActivityContext(Context context) { + if (context instanceof Activity) { + return true; + } else if (context instanceof ContextWrapper) { + return isActivityContext(((ContextWrapper) context).getBaseContext()); + } else { + return false; + } + } + private DialogAction getDialogAction(String action) { return switch (action) { case "positive" -> DialogAction.POSITIVE; diff --git a/app/src/main/java/org/autojs/autojs/external/tasker/TaskerScriptEditActivity.java b/app/src/main/java/org/autojs/autojs/external/tasker/TaskerScriptEditActivity.java index 06d9c3ab..a66c4ad7 100644 --- a/app/src/main/java/org/autojs/autojs/external/tasker/TaskerScriptEditActivity.java +++ b/app/src/main/java/org/autojs/autojs/external/tasker/TaskerScriptEditActivity.java @@ -53,7 +53,7 @@ public class TaskerScriptEditActivity extends BaseActivity { ViewUtils.showToast(TaskerScriptEditActivity.this, ex.getMessage(), true); finish(); }); - setToolbarAsBack(mEditorView.name); + setToolbarAsBack(mEditorView.getName()); } public static void edit(Activity activity, String title, String summary, String content) { diff --git a/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java b/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java index b8a3f861..438bb50d 100644 --- a/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java +++ b/app/src/main/java/org/autojs/autojs/network/UpdateChecker.java @@ -29,7 +29,7 @@ import org.autojs.autojs.network.download.DownloadManager; import org.autojs.autojs.network.entity.ExtendedVersionInfo; import org.autojs.autojs.network.entity.VersionInfo; import org.autojs.autojs.tool.SimpleObserver; -import org.autojs.autojs.ui.settings.DisplayVersionHistoriesActivity; +import org.autojs.autojs.ui.settings.DisplayReleaseHistoryActivity; import org.autojs.autojs.util.AndroidUtils; import org.autojs.autojs.util.IntentUtils; import org.autojs.autojs.util.IntentUtils.SnackExceptionHolder; @@ -857,8 +857,8 @@ public class UpdateChecker { }) .show(); }), - new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_version_histories), parentDialog -> { - DisplayVersionHistoriesActivity.launch(context); + new MaterialDialog.OptionMenuItemSpec(context.getString(R.string.dialog_button_release_history), parentDialog -> { + DisplayReleaseHistoryActivity.launch(context); }) )) .content(R.string.text_retrieving_changelog) diff --git a/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java b/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java index 6817eca7..829e58ff 100644 --- a/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java +++ b/app/src/main/java/org/autojs/autojs/network/download/DownloadManager.java @@ -22,7 +22,7 @@ import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import org.autojs.autojs.app.DialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.network.UpdateChecker; import org.autojs.autojs.network.api.DownloadApi; import org.autojs.autojs.network.entity.VersionInfo; diff --git a/app/src/main/java/org/autojs/autojs/permission/AllFilesAccessPermission.kt b/app/src/main/java/org/autojs/autojs/permission/AllFilesAccessPermission.kt index 8388065d..190bf618 100644 --- a/app/src/main/java/org/autojs/autojs/permission/AllFilesAccessPermission.kt +++ b/app/src/main/java/org/autojs/autojs/permission/AllFilesAccessPermission.kt @@ -10,7 +10,7 @@ import android.os.Environment import android.provider.Settings import androidx.annotation.RequiresApi import androidx.core.app.ActivityCompat -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.permission.Base.getPermissionsToRequest import org.autojs.autojs.ui.common.NotAskAgainDialog import org.autojs.autojs.ui.main.MainActivity diff --git a/app/src/main/java/org/autojs/autojs/permission/DisplayOverOtherAppsPermission.kt b/app/src/main/java/org/autojs/autojs/permission/DisplayOverOtherAppsPermission.kt index 98bf62cb..9f88078f 100644 --- a/app/src/main/java/org/autojs/autojs/permission/DisplayOverOtherAppsPermission.kt +++ b/app/src/main/java/org/autojs/autojs/permission/DisplayOverOtherAppsPermission.kt @@ -4,7 +4,7 @@ import android.content.Context import android.os.Handler import android.os.Looper import ezy.assist.compat.SettingsCompat -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.ui.common.NotAskAgainDialog import org.autojs.autojs.ui.main.drawer.PermissionItemHelper import org.autojs.autojs.util.IntentUtils diff --git a/app/src/main/java/org/autojs/autojs/permission/PostNotificationsPermission.kt b/app/src/main/java/org/autojs/autojs/permission/PostNotificationsPermission.kt index 70648dd0..1e2b15bc 100644 --- a/app/src/main/java/org/autojs/autojs/permission/PostNotificationsPermission.kt +++ b/app/src/main/java/org/autojs/autojs/permission/PostNotificationsPermission.kt @@ -11,7 +11,7 @@ import org.autojs.autojs.core.pref.Pref import org.autojs.autojs.runtime.api.Permissions import org.autojs.autojs.ui.main.drawer.PermissionItemHelper import org.autojs.autojs.util.ContextUtils.findActivity -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.util.NotificationUtils import org.autojs.autojs6.R import java.util.WeakHashMap diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt index c9a63ed8..6bdb6d65 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/DevPluginService.kt @@ -162,7 +162,7 @@ class DevPluginService(val context: Context) { .sayHello() } else { if (client.isExtensionVersionCheckFailed || client.hasErrorMessageOnHello) { - JsonSocketClient.addIntoHistories(ip) + JsonSocketClient.addIntoHistory(ip) client.switchOff() return@doOnNext } diff --git a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt index 58124251..a02504b5 100644 --- a/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt +++ b/app/src/main/java/org/autojs/autojs/pluginclient/JsonSocketClient.kt @@ -321,20 +321,20 @@ class JsonSocketClient(service: DevPluginService?, private val ctx: Context, pri // zh-CN: 连接超时 = 握手超时 + 5 秒 (默认). private const val CONNECT_TIMEOUT = HANDSHAKE_TIMEOUT + 5_000 - var serverAddressHistories: LinkedHashSet - get() = Pref.getLinkedHashSet(R.string.key_pc_server_address_histories) - private set(value) = Pref.putLinkedHashSet(R.string.key_pc_server_address_histories, value) + var serverAddressHistory: LinkedHashSet + get() = Pref.getLinkedHashSet(R.string.key_pc_server_address_history) + private set(value) = Pref.putLinkedHashSet(R.string.key_pc_server_address_history, value) - fun addIntoHistories(ip: String) { - serverAddressHistories = linkedSetOf(ip).apply { addAll(serverAddressHistories) } + fun addIntoHistory(ip: String) { + serverAddressHistory = linkedSetOf(ip).apply { addAll(serverAddressHistory) } } - fun removeFromHistories(ip: String) { - serverAddressHistories = serverAddressHistories.apply { remove(ip) } + fun removeFromHistory(ip: String) { + serverAddressHistory = serverAddressHistory.apply { remove(ip) } } - fun clearAllHistories() { - serverAddressHistories = linkedSetOf() + fun clearHistory() { + serverAddressHistory = linkedSetOf() } // Track current active instance for guarding state emission. diff --git a/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt b/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt index 0d4cc698..ebe8b159 100644 --- a/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt +++ b/app/src/main/java/org/autojs/autojs/rhino/AndroidClassLoader.kt @@ -65,7 +65,7 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir: Log.d(TAG, "loadDex: file = $originalPath") if (!file.exists() || !file.canRead()) { - throw FileNotFoundException(str(R.string.file_not_exist_or_readable, originalPath)) + throw FileNotFoundException(str(R.string.file_not_exist_or_readable_with_path, originalPath)) } // @Hint by SuperMonster003 on Nov 30, 2023. @@ -99,7 +99,7 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir: fun loadJar(jar: File): DexClassLoader { Log.d(TAG, "loadJar: jar = ${jar.path}") if (!jar.exists() || !jar.canRead()) { - throw FileNotFoundException(str(R.string.file_not_exist_or_readable, jar.path)) + throw FileNotFoundException(str(R.string.file_not_exist_or_readable_with_path, jar.path)) } val key = generateArtifactsCacheName(jar) diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupScheduler.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupScheduler.kt index 4de39d9a..c779af0d 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupScheduler.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupScheduler.kt @@ -3,7 +3,9 @@ package org.autojs.autojs.storage.history import android.content.Context import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import java.util.concurrent.TimeUnit @@ -13,16 +15,38 @@ import java.util.concurrent.TimeUnit * zh-CN: HistoryCleanupWorker 的调度器. * * Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. */ object HistoryCleanupScheduler { private const val UNIQUE_WORK_NAME = "history_cleanup_periodic" + fun scheduleStartupCleanup(context: Context) { + val appContext = context.applicationContext + + // Enqueue a one-time startup cleanup for stale partial trash blobs. + // zh-CN: 排队一个启动时的一次性清理任务, 用于清理过期的 partial 回收站 blob. + runCatching { + val req = OneTimeWorkRequestBuilder() + // Delay a bit to reduce cold-start I/O pressure. + // zh-CN: 适当延迟以降低冷启动 I/O 压力. + .setInitialDelay(10L, TimeUnit.SECONDS) + .build() + + WorkManager.getInstance(appContext) + .enqueueUniqueWork( + "startup-trash-partial-cleanup", + ExistingWorkPolicy.KEEP, + req, + ) + } + } + /** * Schedule periodic cleanup (once per day). * zh-CN: 调度周期性清理 (每天一次). */ - fun schedule(context: Context) { + fun schedulePeriodicCleanup(context: Context) { val appContext = context.applicationContext // No network is required; keep it light and battery-friendly. diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupWorker.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupWorker.kt index 7a454b7c..67e031c2 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupWorker.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryCleanupWorker.kt @@ -3,6 +3,7 @@ package org.autojs.autojs.storage.history import android.content.Context import androidx.work.CoroutineWorker import androidx.work.WorkerParameters +import org.autojs.autojs.storage.history.TrashBlobStore.Companion.TRASH_BLOB_DIR_REL import java.io.File /** @@ -10,6 +11,7 @@ import java.io.File * zh-CN: 用于 history/trash/drafts 的周期性清理 Worker. * * Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026. + * Modified by SuperMonster003 as of Feb 8, 2026. */ class HistoryCleanupWorker( appContext: Context, @@ -20,6 +22,7 @@ class HistoryCleanupWorker( return runCatching { cleanupExpiredRevisions() cleanupExpiredTrashItems() + cleanupTrashTotalBytesLimit() cleanupOrphanHistoryBlobs() cleanupOrphanTrashBlobs() cleanupEmergencyDrafts() @@ -34,10 +37,12 @@ class HistoryCleanupWorker( val dao = db.historyDao() val blobs = HistoryBlobStore(applicationContext) - // Remove expired revisions by days (30 days). - // zh-CN: 按天数删除过期 revision (30 天). + // Remove expired revisions by days. + // zh-CN: 按天数删除过期 revision. val now = System.currentTimeMillis() - val expiredBefore = now - MAX_DAYS_MS + val maxDaysMs = HistoryPrefs.historyMaxDays().coerceAtLeast(0).toLong() * 24L * 60L * 60L * 1000L + val expiredBefore = now - maxDaysMs + val expired = dao.listExpiredRevisions(expiredBefore) if (expired.isEmpty()) return @@ -45,6 +50,52 @@ class HistoryCleanupWorker( expired.forEach { blobs.deleteBlobByRelPath(it.blobRelPath) } } + private fun cleanupExpiredTrashItems() { + val db = HistoryDatabase.getInstance(applicationContext) + val dao = db.historyDao() + val trashBlobs = TrashBlobStore(applicationContext) + + // Remove expired trash items by days. + // zh-CN: 按天数删除过期回收站条目. + val now = System.currentTimeMillis() + val maxDaysMs = HistoryPrefs.trashMaxDays().coerceAtLeast(0).toLong() * 24L * 60L * 60L * 1000L + val expiredBefore = now - maxDaysMs + + val expired = dao.listExpiredTrashItems(expiredBefore) + if (expired.isEmpty()) return + + dao.deleteTrashItemsByIds(expired.map { it.trashId }) + expired.forEach { trashBlobs.deleteBlobByRelPath(it.blobRelPath) } + } + + /** + * Enforce trash total bytes limit by deleting oldest items first. + * zh-CN: 通过优先删除最旧条目来约束回收站总容量上限. + */ + private fun cleanupTrashTotalBytesLimit() { + val limit = HistoryPrefs.trashMaxTotalBytes().coerceAtLeast(0L) + if (limit <= 0L) return + + val db = HistoryDatabase.getInstance(applicationContext) + val dao = db.historyDao() + val trashBlobs = TrashBlobStore(applicationContext) + + val items = dao.listTrashItemsDesc() + if (items.isEmpty()) return + + var total = items.sumOf { it.sizeBytes.coerceAtLeast(0L) } + if (total <= limit) return + + // Delete from oldest to newest until within limit. + // zh-CN: 从最旧到最新删除, 直到满足上限. + for (item in items.asReversed()) { + if (total <= limit) break + dao.deleteTrashItemsByIds(listOf(item.trashId)) + trashBlobs.deleteBlobByRelPath(item.blobRelPath) + total -= item.sizeBytes.coerceAtLeast(0L) + } + } + private fun cleanupOrphanHistoryBlobs() { val db = HistoryDatabase.getInstance(applicationContext) val dao = db.historyDao() @@ -81,22 +132,6 @@ class HistoryCleanupWorker( } } - private fun cleanupExpiredTrashItems() { - val db = HistoryDatabase.getInstance(applicationContext) - val dao = db.historyDao() - val trashBlobs = TrashBlobStore(applicationContext) - - // Remove expired trash items by days (default 30 days). - // zh-CN: 按天数删除过期回收站条目 (默认 30 天). - val now = System.currentTimeMillis() - val expiredBefore = now - TRASH_MAX_DAYS_MS - val expired = dao.listExpiredTrashItems(expiredBefore) - if (expired.isEmpty()) return - - dao.deleteTrashItemsByIds(expired.map { it.trashId }) - expired.forEach { trashBlobs.deleteBlobByRelPath(it.blobRelPath) } - } - private fun cleanupOrphanTrashBlobs() { val db = HistoryDatabase.getInstance(applicationContext) val dao = db.historyDao() @@ -105,14 +140,23 @@ class HistoryCleanupWorker( // zh-CN: 从 DB 构建被引用的回收站 blob 集合. val referenced = dao.listAllTrashBlobRelPaths().toHashSet() - val root = File(applicationContext.filesDir, "trash/blob") + val root = File(applicationContext.filesDir, TRASH_BLOB_DIR_REL) if (!root.exists() || !root.isDirectory) return + val now = System.currentTimeMillis() + // Delete trash blobs that are not referenced by DB. // zh-CN: 删除 DB 未引用的回收站 blob. root.walkTopDown() .filter { it.isFile } .forEach { f -> + // Protect very new files to avoid deleting blobs created while DB write is pending. + // zh-CN: 保护非常新的文件, 避免在 DB 写入尚未完成时误删刚创建的 blob. + val ageMs = now - f.lastModified() + if (ageMs >= 0L && ageMs < ORPHAN_TRASH_BLOB_MIN_AGE_MS) { + return@forEach + } + val rel = f.relativeTo(applicationContext.filesDir).invariantSeparatorsPath if (!referenced.contains(rel)) { // noinspection ResultOfMethodCallIgnored @@ -134,16 +178,15 @@ class HistoryCleanupWorker( } private fun cleanupEmergencyDrafts() { - // Keep consistent with EditorView's local cleanup policy. - // zh-CN: 与 EditorView 的本地清理策略保持一致. val draftsDir = File(applicationContext.filesDir, "drafts") if (!draftsDir.exists() || !draftsDir.isDirectory) return val now = System.currentTimeMillis() - // 1) Remove expired (older than 7 days). - // zh-CN: 1) 删除过期草稿 (超过 7 天). - val expiredBefore = now - DRAFT_MAX_DAYS_MS + // 1) Remove expired by days. + // zh-CN: 1) 按天数删除过期草稿. + val maxDaysMs = HistoryPrefs.draftsMaxDays().coerceAtLeast(0).toLong() * 24L * 60L * 60L * 1000L + val expiredBefore = now - maxDaysMs draftsDir.listFiles()?.forEach { f -> if (f.isFile && f.lastModified() < expiredBefore) { // noinspection ResultOfMethodCallIgnored @@ -158,11 +201,12 @@ class HistoryCleanupWorker( ?.sortedByDescending { it.lastModified() } ?: return + val limit = HistoryPrefs.draftsMaxTotalBytes().coerceAtLeast(0L) var total = remained.sumOf { it.length().coerceAtLeast(0L) } - if (total <= DRAFT_MAX_TOTAL_BYTES) return + if (total <= limit) return for (f in remained.asReversed()) { - if (total <= DRAFT_MAX_TOTAL_BYTES) break + if (total <= limit) break val len = f.length().coerceAtLeast(0L) if (f.delete()) { total -= len @@ -171,9 +215,8 @@ class HistoryCleanupWorker( } companion object { - private const val MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L - private const val TRASH_MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L - private const val DRAFT_MAX_DAYS_MS: Long = 7L * 24L * 60L * 60L * 1000L - private const val DRAFT_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L + // Minimum age for orphan final trash blobs before deletion to avoid races. + // zh-CN: orphan 正式回收站 blob 的最小删除年龄, 用于避免并发时序竞争. + private const val ORPHAN_TRASH_BLOB_MIN_AGE_MS: Long = 30L * 60L * 1000L } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt index dedeb8db..119b11f0 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDao.kt @@ -4,9 +4,11 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import io.reactivex.Flowable /** * Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 7, 2026. */ @Dao interface HistoryDao { @@ -32,6 +34,26 @@ interface HistoryDao { @Query("SELECT SUM(sizeBytes) FROM revision WHERE fileId = :fileId") fun sumBytesByFileId(fileId: String): Long? + /** + * Sum revision bytes for multiple fileIds in one query. + * zh-CN: 在一次查询中批量统计多个 fileId 的 revision 字节总和. + */ + @Query( + "SELECT fileId AS fileId, SUM(sizeBytes) AS totalBytes " + + "FROM revision WHERE fileId IN (:fileIds) GROUP BY fileId" + ) + fun sumBytesByFileIds(fileIds: List): List + + /** + * Aggregate revision stats for multiple fileIds in one query. + * zh-CN: 在一次查询中批量聚合多个 fileId 的 revision 统计信息. + */ + @Query( + "SELECT fileId AS fileId, COUNT(*) AS revisionCount, SUM(sizeBytes) AS totalBytes " + + "FROM revision WHERE fileId IN (:fileIds) GROUP BY fileId" + ) + fun aggStatsByFileIds(fileIds: List): List + @Query("SELECT * FROM file_entry") fun listAllFiles(): List @@ -55,4 +77,85 @@ interface HistoryDao { @Query("SELECT blobRelPath FROM trash_item") fun listAllTrashBlobRelPaths(): List -} \ No newline at end of file + + @Query("DELETE FROM trash_item") + fun deleteAllTrashItems() + + @Query("SELECT * FROM file_entry WHERE logicalPath LIKE :pathPrefixEscape || '%'") + fun listFilesByPathPrefix(pathPrefixEscape: String): List + + @Query("DELETE FROM file_entry WHERE fileId IN (:fileIds)") + fun deleteFileEntriesByIds(fileIds: List) + + @Query("SELECT * FROM revision WHERE fileId IN (:fileIds) ORDER BY createdAt ASC") + fun listRevisionsByFileIds(fileIds: List): List + + @Query("DELETE FROM revision WHERE fileId IN (:fileIds)") + fun deleteRevisionsByFileIds(fileIds: List) + + @Query("SELECT SUM(sizeBytes) FROM revision") + fun sumAllRevisionBytes(): Long? + + @Query("SELECT * FROM revision ORDER BY createdAt ASC LIMIT :limit") + fun listOldestRevisions(limit: Int): List + + /** + * Aggregated stats for trash. + * zh-CN: 回收站聚合统计. + */ + @Query("SELECT COUNT(*) AS count, SUM(sizeBytes) AS totalBytes FROM trash_item") + fun observeTrashStats(): Flowable + + /** + * Aggregated stats for version history (all revisions). + * zh-CN: 版本历史聚合统计 (所有 revisions). + */ + @Query("SELECT COUNT(*) AS fileCount, (SELECT SUM(sizeBytes) FROM revision) AS totalBytes FROM file_entry") + fun observeVersionHistoryStats(): Flowable + + @Query("SELECT * FROM revision ORDER BY createdAt ASC") + fun listAllRevisionsAsc(): List + + @Query("DELETE FROM revision") + fun deleteAllRevisions() + + @Query("DELETE FROM file_entry") + fun deleteAllFileEntries() +} + +/** + * Aggregated bytes row for revisions. + * zh-CN: revision 的聚合字节统计行. + */ +data class FileBytesSumRow( + val fileId: String, + val totalBytes: Long?, +) + +/** + * Aggregated stats row for revisions by fileId. + * zh-CN: 按 fileId 聚合的 revision 统计行. + */ +data class FileAggStatsRow( + val fileId: String, + val revisionCount: Long, + val totalBytes: Long?, +) + +/** + * Aggregated stats row for trash. + * zh-CN: 回收站统计行. + */ +data class TrashStatsRow( + val count: Long, + val totalBytes: Long?, +) + +/** + * Aggregated stats row for version history. + * zh-CN: 版本历史统计行. + */ +data class VersionHistoryStatsRow( + val fileCount: Long, + val totalBytes: Long?, +) diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt index c78885c6..5e998267 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryDatabase.kt @@ -61,4 +61,4 @@ abstract class HistoryDatabase : RoomDatabase() { }.build().also { instance = it } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryEntities.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryEntities.kt index afeef36a..0b8d4036 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryEntities.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryEntities.kt @@ -1,11 +1,13 @@ package org.autojs.autojs.storage.history +import android.net.Uri import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey /** * Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026. + * Modified by SuperMonster003 as of Feb 6, 2026. */ object HistoryEntities { @@ -29,7 +31,13 @@ object HistoryEntities { // Latest content fingerprint for "path lost but content same" matching. // zh-CN: 用于 "路径丢失但内容相同" 匹配的最新内容指纹. val latestFingerprint: String, - ) + ) { + val displayPath: String + get() = runCatching { Uri.decode(logicalPath) }.getOrElse { logicalPath } + + val canonicalPath: String + get() = logicalPath + } @Entity( tableName = "revision", diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryPrefs.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryPrefs.kt new file mode 100644 index 00000000..3cc37e2f --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryPrefs.kt @@ -0,0 +1,78 @@ +package org.autojs.autojs.storage.history + +import org.autojs.autojs.core.pref.Pref +import org.autojs.autojs6.R + +/** + * Preferences for history/trash/drafts policies. + * zh-CN: history/trash/drafts 策略相关的偏好设置读取. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 5, 2026. + */ +object HistoryPrefs { + + // Max file size to track history snapshots (default 20MB). + // zh-CN: 纳管历史快照的最大文件大小 (默认 20MB). + fun maxFileSizeToTrackBytes(): Long = + Pref.getLong(R.string.key_history_max_file_size_to_track_bytes, DEFAULT_MAX_FILE_SIZE_TO_TRACK_BYTES) + + // History retention days (default 30). + // zh-CN: 历史保留天数 (默认 30). + fun historyMaxDays(): Int = + Pref.getInt(R.string.key_history_max_days, DEFAULT_HISTORY_MAX_DAYS) + + // History max versions per file (default 50). + // zh-CN: 单文件历史最大版本数 (默认 50). + fun historyMaxVersions(): Int = + Pref.getInt(R.string.key_history_max_versions, DEFAULT_HISTORY_MAX_VERSIONS) + + // History max total bytes per file (default 200MB). + // zh-CN: 单文件历史总容量上限 (默认 200MB). + fun historyMaxTotalBytesPerFile(): Long = + Pref.getLong(R.string.key_history_max_total_bytes_per_file, DEFAULT_HISTORY_MAX_TOTAL_BYTES_PER_FILE) + + // History max total bytes (default 2GB). + // zh-CN: 历史总容量上限 (默认 2GB). + fun historyMaxTotalBytes(): Long = + Pref.getLong(R.string.key_history_max_total_bytes, DEFAULT_HISTORY_MAX_TOTAL_BYTES) + + // Trash retention days (default 30). + // zh-CN: 回收站保留天数 (默认 30). + fun trashMaxDays(): Int = + Pref.getInt(R.string.key_trash_max_days, DEFAULT_TRASH_MAX_DAYS) + + // Trash max total bytes (default 500MB). + // zh-CN: 回收站总容量上限 (默认 500MB). + fun trashMaxTotalBytes(): Long = + Pref.getLong(R.string.key_trash_max_total_bytes, DEFAULT_TRASH_MAX_TOTAL_BYTES) + + // Draft retention days (default 7). + // zh-CN: 草稿保留天数 (默认 7). + fun draftsMaxDays(): Int = + Pref.getInt(R.string.key_drafts_max_days, DEFAULT_DRAFTS_MAX_DAYS) + + // Draft max total bytes (default 200MB). + // zh-CN: 草稿总容量上限 (默认 200MB). + fun draftsMaxTotalBytes(): Long = + Pref.getLong(R.string.key_drafts_max_total_bytes, DEFAULT_DRAFTS_MAX_TOTAL_BYTES) + + // Whether to clear all history when deleting permanently (default false). + // zh-CN: 是否在彻底删除时同时清除历史记录 (默认 false). + @JvmStatic + fun deletePermanentlyAlsoClearHistory(): Boolean = + Pref.getBoolean(R.string.key_delete_permanently_also_clear_history, false) + + private const val DEFAULT_MAX_FILE_SIZE_TO_TRACK_BYTES: Long = 20L * 1024L * 1024L + + private const val DEFAULT_HISTORY_MAX_DAYS: Int = 30 + private const val DEFAULT_HISTORY_MAX_VERSIONS: Int = 50 + internal const val DEFAULT_HISTORY_MAX_TOTAL_BYTES_PER_FILE: Long = 200L * 1024L * 1024L + internal const val DEFAULT_HISTORY_MAX_TOTAL_BYTES: Long = 2L * 1024L * 1024L * 1024L + + private const val DEFAULT_TRASH_MAX_DAYS: Int = 30 + internal const val DEFAULT_TRASH_MAX_TOTAL_BYTES: Long = 500L * 1024L * 1024L + + private const val DEFAULT_DRAFTS_MAX_DAYS: Int = 7 + private const val DEFAULT_DRAFTS_MAX_TOTAL_BYTES: Long = 200L * 1024L * 1024L +} diff --git a/app/src/main/java/org/autojs/autojs/storage/history/HistoryRepositoryHandler.kt b/app/src/main/java/org/autojs/autojs/storage/history/HistoryRepository.kt similarity index 56% rename from app/src/main/java/org/autojs/autojs/storage/history/HistoryRepositoryHandler.kt rename to app/src/main/java/org/autojs/autojs/storage/history/HistoryRepository.kt index 63904ddc..22c1e7e9 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/HistoryRepositoryHandler.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/HistoryRepository.kt @@ -1,13 +1,13 @@ package org.autojs.autojs.storage.history import android.content.Context -import android.net.Uri import java.io.File import java.security.MessageDigest import java.util.Locale /** * Created by JetBrains AI Assistant (GPT-5.2) on Feb 3, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 6, 2026. */ class HistoryRepository(private val context: Context) { @@ -42,7 +42,6 @@ class HistoryRepository(private val context: Context) { * - 插入后执行单文件保留策略清理. */ fun recordSavePre( - uri: Uri, logicalPath: String, oldBytes: ByteArray, encodingName: String, @@ -76,6 +75,41 @@ class HistoryRepository(private val context: Context) { ) cleanupPerFile(fileEntry.fileId, now) + cleanupGlobal() + } + + /** + * Clear all history revisions/blobs for a given path. + * + * Rules: + * - If target is a file: clear exact match. + * - If target is a directory: clear all entries under this directory by path prefix. + * + * zh-CN: + * + * 清除指定路径对应的全部历史版本与 blob. + * + * 规则: + * - 若目标是文件: 精确匹配清理. + * - 若目标是目录: 按路径前缀清理该目录下所有已纳管条目. + */ + fun clearHistoryForPath(path: String) { + val normalized = path.trimEnd('/') + + val target = File(normalized) + val fileIds = if (target.isDirectory) { + val prefix = "$normalized/" + dao.listFilesByPathPrefix(prefix).map { it.fileId } + } else { + dao.findFileByPath(normalized)?.let { listOf(it.fileId) } ?: emptyList() + } + + if (fileIds.isEmpty()) return + + val revisions = dao.listRevisionsByFileIds(fileIds) + dao.deleteRevisionsByFileIds(fileIds) + revisions.forEach { blobs.deleteBlobByRelPath(it.blobRelPath) } + dao.deleteFileEntriesByIds(fileIds) } private fun getOrCreateFileEntry(logicalPath: String, latestFingerprint: String, now: Long): HistoryEntities.FileEntry { @@ -117,9 +151,15 @@ class HistoryRepository(private val context: Context) { val revisions = dao.listRevisionsAsc(fileId).toMutableList() if (revisions.isEmpty()) return + // Read policy from preferences. + // zh-CN: 从偏好设置读取策略参数. + val maxDaysMs = HistoryPrefs.historyMaxDays().coerceAtLeast(0).toLong() * 24L * 60L * 60L * 1000L + val maxVersions = HistoryPrefs.historyMaxVersions().coerceAtLeast(0) + val maxTotalBytesPerFile = HistoryPrefs.historyMaxTotalBytesPerFile().coerceAtLeast(0L) + // 1) Remove expired by days. // zh-CN: 1) 按天数删除过期版本. - val expiredBefore = now - MAX_DAYS_MS + val expiredBefore = now - maxDaysMs val expired = revisions.filter { it.createdAt < expiredBefore } if (expired.isNotEmpty()) { deleteRevisionsAndBlobs(expired) @@ -128,8 +168,8 @@ class HistoryRepository(private val context: Context) { // 2) Enforce max count (remove oldest). // zh-CN: 2) 约束最大版本数 (删除最旧). - if (revisions.size > MAX_VERSIONS) { - val toRemove = revisions.take(revisions.size - MAX_VERSIONS) + if (revisions.size > maxVersions) { + val toRemove = revisions.take(revisions.size - maxVersions) deleteRevisionsAndBlobs(toRemove) revisions.removeAll(toRemove.toSet()) } @@ -137,9 +177,9 @@ class HistoryRepository(private val context: Context) { // 3) Enforce max total bytes per file (remove oldest). // zh-CN: 3) 约束单文件历史总容量 (删除最旧). var totalBytes = (dao.sumBytesByFileId(fileId) ?: 0L).coerceAtLeast(0L) - if (totalBytes > MAX_TOTAL_BYTES_PER_FILE) { + if (totalBytes > maxTotalBytesPerFile) { for (rev in revisions.toList()) { - if (totalBytes <= MAX_TOTAL_BYTES_PER_FILE) break + if (totalBytes <= maxTotalBytesPerFile) break deleteRevisionsAndBlobs(listOf(rev)) totalBytes -= rev.sizeBytes.coerceAtLeast(0L) revisions.remove(rev) @@ -147,6 +187,31 @@ class HistoryRepository(private val context: Context) { } } + /** + * Delete a single revision and its blob. + * zh-CN: 删除单个 revision 及其 blob. + */ + fun deleteRevision(rev: HistoryEntities.Revision) { + deleteRevisionsAndBlobs(listOf(rev)) + } + + /** + * Clear all version history (all file entries, all revisions, all blobs). + * zh-CN: 清空全部版本历史 (所有 file_entry, 所有 revision, 所有 blobs). + */ + fun clearAllHistory() { + val revs = dao.listAllRevisionsAsc() + if (revs.isNotEmpty()) { + revs.forEach { blobs.deleteBlobByRelPath(it.blobRelPath) } + } + dao.deleteAllRevisions() + dao.deleteAllFileEntries() + } + + fun getRevisionsCount(fileId: String): Int { + return dao.listRevisionsAsc(fileId).size + } + private fun deleteRevisionsAndBlobs(revs: List) { if (revs.isEmpty()) return dao.deleteRevisionsByIds(revs.map { it.revId }) @@ -162,9 +227,41 @@ class HistoryRepository(private val context: Context) { return sb.toString() } - companion object { - private const val MAX_DAYS_MS: Long = 30L * 24L * 60L * 60L * 1000L - private const val MAX_VERSIONS: Int = 50 - private const val MAX_TOTAL_BYTES_PER_FILE: Long = 200L * 1024L * 1024L + /** + * Enforce global history total bytes limit (remove oldest revisions first). + * zh-CN: 约束全局历史总容量上限 (优先删除最旧 revision). + */ + private fun cleanupGlobal() { + val maxTotalBytes = HistoryPrefs.historyMaxTotalBytes().coerceAtLeast(0L) + if (maxTotalBytes <= 0L) return + + var totalBytes = (dao.sumAllRevisionBytes() ?: 0L).coerceAtLeast(0L) + if (totalBytes <= maxTotalBytes) return + + // Delete in batches to reduce DB roundtrips. + // zh-CN: 批量删除以减少 DB 往返次数. + val batchSize = 50 + + while (totalBytes > maxTotalBytes) { + val oldest = dao.listOldestRevisions(batchSize) + if (oldest.isEmpty()) break + + // Remove until under limit or batch exhausted. + // zh-CN: 删除直到满足上限或本批用尽. + val toRemove = ArrayList(oldest.size) + var bytesToFree = 0L + + for (rev in oldest) { + if (totalBytes - bytesToFree <= maxTotalBytes) break + toRemove.add(rev) + bytesToFree += rev.sizeBytes.coerceAtLeast(0L) + } + + if (toRemove.isEmpty()) break + + deleteRevisionsAndBlobs(toRemove) + + totalBytes = (totalBytes - bytesToFree).coerceAtLeast(0L) + } } } diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStore.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStore.kt new file mode 100644 index 00000000..51e81def --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStore.kt @@ -0,0 +1,136 @@ +package org.autojs.autojs.storage.history + +import android.content.Context +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.OutputStream +import java.util.UUID +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +/** + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 7, 2026. + */ +class TrashBlobStore(private val context: Context) { + + fun newTrashId(): String = UUID.randomUUID().toString() + + /** + * Open an OutputStream for writing trash blob into a temporary file, returns tempRelPath and stream. + * zh-CN: 打开用于写入回收站 blob 的临时文件输出流, 并返回 tempRelPath 与 stream. + */ + fun openTrashBlobTempOutputStream(trashId: String): Pair { + val tempRel = tempTrashBlobRelPath(trashId) + val f = File(context.filesDir, tempRel) + f.parentFile?.mkdirs() + return tempRel to f.outputStream() + } + + /** + * Commit a temporary trash blob into its final path by atomic rename, returns final relPath. + * zh-CN: 通过原子 rename 将临时回收站 blob 提交到最终路径, 并返回最终 relPath. + */ + fun commitTrashBlobTemp(tempRelPath: String, trashId: String): String { + val finalRel = finalTrashBlobRelPath(trashId) + + val tempFile = File(context.filesDir, tempRelPath) + val finalFile = File(context.filesDir, finalRel) + + require(tempFile.exists()) { "Temp trash blob not exists: $tempRelPath" } + + finalFile.parentFile?.mkdirs() + + if (finalFile.exists()) { + // Remove existing final blob first to ensure rename success. + // zh-CN: 先删除已存在的最终 blob, 以确保 rename 能成功. + // noinspection ResultOfMethodCallIgnored + finalFile.delete() + } + + // Rename in the same directory is expected to be atomic on Android/Linux. + // zh-CN: 同目录 rename 在 Android/Linux 上通常具备原子性. + if (!tempFile.renameTo(finalFile)) { + throw IllegalStateException("Commit trash blob failed: $tempFile -> $finalFile") + } + + return finalRel + } + + fun writeTrashBlob(trashId: String, bytes: ByteArray): String { + val rel = finalTrashBlobRelPath(trashId) + val f = File(context.filesDir, rel) + f.parentFile?.mkdirs() + f.outputStream().use { it.write(bytes) } + return rel + } + + fun deleteBlobByRelPath(relPath: String) { + val f = File(context.filesDir, relPath) + if (f.exists()) { + // noinspection ResultOfMethodCallIgnored + f.delete() + } + } + + /** + * Cleanup stale temporary trash blobs (partial files). + * zh-CN: 清理过期的临时回收站 blob (partial 文件). + */ + fun cleanupStalePartialTrashBlobs(maxAgeMs: Long) { + val dir = File(context.filesDir, TRASH_BLOB_DIR_REL) + if (!dir.exists() || !dir.isDirectory) return + + val now = System.currentTimeMillis() + val files = dir.listFiles() ?: return + + for (f in files) { + if (!f.isFile) continue + if (!f.name.contains(PARTIAL_MARK)) continue + + val age = now - f.lastModified() + if (age < maxAgeMs) continue + + // Best-effort delete, ignore result. + // zh-CN: 尽力删除, 忽略结果. + runCatching { + // noinspection ResultOfMethodCallIgnored + f.delete() + } + } + } + + private fun finalTrashBlobRelPath(trashId: String): String = + "$TRASH_BLOB_DIR_REL/$trashId.bin" + + private fun tempTrashBlobRelPath(trashId: String): String = + "$TRASH_BLOB_DIR_REL/$trashId$PARTIAL_MARK.${UUID.randomUUID()}.bin" + + /** + * Pack a directory into zip bytes. + * zh-CN: 将目录打包为 zip bytes. + */ + fun zipDirectoryToBytes(dir: File): ByteArray { + require(dir.isDirectory) { "Not a directory: $dir" } + + val baos = ByteArrayOutputStream() + ZipOutputStream(baos).use { zos -> + val basePathLen = dir.absolutePath.trimEnd('/').length + 1 + dir.walkTopDown().forEach { f -> + if (!f.isFile) return@forEach + val relPath = f.absolutePath.substring(basePathLen).replace(File.separatorChar, '/') + val entry = ZipEntry(relPath) + zos.putNextEntry(entry) + FileInputStream(f).use { it.copyTo(zos) } + zos.closeEntry() + } + } + return baos.toByteArray() + } + + companion object { + const val TRASH_BLOB_DIR_REL: String = "trash/blob" + const val PARTIAL_MARK: String = ".partial" + } +} diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt deleted file mode 100644 index 65eeda0e..00000000 --- a/app/src/main/java/org/autojs/autojs/storage/history/TrashBlobStoreUtils.kt +++ /dev/null @@ -1,55 +0,0 @@ -package org.autojs.autojs.storage.history - -import android.content.Context -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileInputStream -import java.util.UUID -import java.util.zip.ZipEntry -import java.util.zip.ZipOutputStream - -/** - * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. - */ -class TrashBlobStore(private val context: Context) { - - fun newTrashId(): String = UUID.randomUUID().toString() - - fun writeTrashBlob(trashId: String, bytes: ByteArray): String { - val rel = "trash/blob/$trashId.bin" - val f = File(context.filesDir, rel) - f.parentFile?.mkdirs() - f.outputStream().use { it.write(bytes) } - return rel - } - - fun deleteBlobByRelPath(relPath: String) { - val f = File(context.filesDir, relPath) - if (f.exists()) { - // noinspection ResultOfMethodCallIgnored - f.delete() - } - } - - /** - * Pack a directory into zip bytes. - * zh-CN: 将目录打包为 zip bytes. - */ - fun zipDirectoryToBytes(dir: File): ByteArray { - require(dir.isDirectory) { "Not a directory: $dir" } - - val baos = ByteArrayOutputStream() - ZipOutputStream(baos).use { zos -> - val basePathLen = dir.absolutePath.trimEnd('/').length + 1 - dir.walkTopDown().forEach { f -> - if (!f.isFile) return@forEach - val relPath = f.absolutePath.substring(basePathLen).replace(File.separatorChar, '/') - val entry = ZipEntry(relPath) - zos.putNextEntry(entry) - FileInputStream(f).use { it.copyTo(zos) } - zos.closeEntry() - } - } - return baos.toByteArray() - } -} diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashPartialCleanupWorker.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashPartialCleanupWorker.kt new file mode 100644 index 00000000..884e084a --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashPartialCleanupWorker.kt @@ -0,0 +1,33 @@ +package org.autojs.autojs.storage.history + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters + +/** + * One-time startup cleanup worker for stale partial trash blobs. + * zh-CN: 用于启动时一次性清理过期 partial 回收站 blob 的 Worker. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 8, 2026. + */ +class TrashPartialCleanupWorker( + appContext: Context, + params: WorkerParameters, +) : CoroutineWorker(appContext, params) { + + override suspend fun doWork(): Result { + return runCatching { + val maxAgeMs = PARTIAL_TRASH_BLOB_MAX_AGE_MS_STARTUP + TrashBlobStore(applicationContext).cleanupStalePartialTrashBlobs(maxAgeMs) + }.fold( + onSuccess = { Result.success() }, + onFailure = { Result.retry() }, + ) + } + + companion object { + // Maximum age for partial trash blob files before deletion on startup. + // zh-CN: 启动时用于删除 partial 回收站 blob 的最大保留时长. + private const val PARTIAL_TRASH_BLOB_MAX_AGE_MS_STARTUP: Long = 30L * 60L * 1000L + } +} diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt index c2fddf3a..f665893f 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashRepository.kt @@ -1,14 +1,26 @@ package org.autojs.autojs.storage.history import android.content.Context +import org.autojs.autojs.ui.common.ScriptOperations +import org.autojs.autojs.util.DialogUtils +import java.io.BufferedInputStream import java.io.File import java.io.FileInputStream +import java.io.InputStream +import java.io.OutputStream +import java.security.DigestOutputStream import java.security.MessageDigest import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong +import java.util.zip.ZipEntry import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream /** * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 7, 2026. */ class TrashRepository(private val context: Context) { @@ -21,7 +33,7 @@ class TrashRepository(private val context: Context) { * * Notes: * - Only internal storage paths are supported. - * - Directory is archived as zip bytes. + * - Directory is archived as zip stream (not bytes) to avoid OOM. * * zh-CN: * @@ -29,9 +41,31 @@ class TrashRepository(private val context: Context) { * * 注意: * - 仅支持内部存储路径. - * - 目录会被打包为 zip bytes. + * - 目录会以 zip 流形式归档 (不再生成 bytes), 以避免 OOM. */ - fun moveToTrash(path: String): TrashEntities.TrashItem { + /** + * Move a file or directory into trash. + * + * Notes: + * - Only internal storage paths are supported. + * - Directory is archived as zip stream (not bytes) to avoid OOM. + * + * zh-CN: + * + * 将文件或目录移入回收站. + * + * 注意: + * - 仅支持内部存储路径. + * - 目录会以 zip 流形式归档 (不再生成 bytes), 以避免 OOM. + */ + fun moveToTrashWithProgress( + path: String, + progressCallback: ScriptOperations.ProgressCallback?, + fileCallback: ScriptOperations.FileProgressCallback?, + operationController: DialogUtils.OperationController, + ): TrashEntities.TrashItem { + operationController.throwIfCancelled() + val src = File(path) require(src.exists()) { "Source not exists: $path" } @@ -40,38 +74,208 @@ class TrashRepository(private val context: Context) { val trashId = blobs.newTrashId() val now = System.currentTimeMillis() - val (bytes, isDir) = if (src.isDirectory) { - blobs.zipDirectoryToBytes(src) to true + val isDir = src.isDirectory + + val digest = MessageDigest.getInstance("SHA-256") + var sizeBytes = 0L + + // Compute total bytes asynchronously because walking a directory can be expensive. + // zh-CN: 异步计算 totalBytes, 因为目录 walk 可能很耗时. + val totalReady = AtomicBoolean(false) + val totalBytesRef = AtomicLong(0L) + + if (src.isFile) { + totalBytesRef.set(src.length().coerceAtLeast(0L)) + totalReady.set(true) } else { - src.inputStream().use { it.readBytes() } to false + Thread { + runCatching { + fun computeTotalBytesCancellable(f: File): Long { + operationController.throwIfCancelled() + if (!f.exists()) return 0L + if (f.isFile) return f.length().coerceAtLeast(0L) + + var sum = 0L + val children = f.listFiles() ?: return 0L + for (c in children) { + operationController.throwIfCancelled() + sum += computeTotalBytesCancellable(c) + } + return sum + } + + val total = computeTotalBytesCancellable(src).coerceAtLeast(0L) + totalBytesRef.set(total) + totalReady.set(true) + } + }.apply { + // Best-effort background computation; should not block main operation. + // zh-CN: 尽力而为的后台计算, 不应阻塞主流程. + isDaemon = true + name = "TrashTotalBytes-$trashId" + start() + } } - val sha = sha256Hex(bytes) - val rel = blobs.writeTrashBlob(trashId, bytes) + val processedBytes = AtomicLong(0L) - // Delete original after blob persisted. - // zh-CN: 在 blob 落盘后删除源文件/目录. - val deleted = if (src.isDirectory) { - src.deleteRecursively() - } else { - src.delete() - } - if (!deleted && src.exists()) { - throw IllegalStateException("Failed to delete source after trash: $src") + // Report progress; if total not ready, pass 0 as requested. + // zh-CN: 上报进度; 若 total 尚未计算完成, 按约定传入 0. + fun reportProcessed(delta: Int) { + if (delta <= 0) return + val p = processedBytes.addAndGet(delta.toLong()) + val t = if (totalReady.get()) totalBytesRef.get().coerceAtLeast(0L) else 0L + progressCallback?.onProgress(p.coerceAtLeast(0L), t) } - val item = TrashEntities.TrashItem( - trashId = trashId, - originalPath = logicalPath, - fileId = null, - trashedAt = now, - isDirectory = isDir, - sizeBytes = bytes.size.toLong(), - sha256 = sha, - blobRelPath = rel, - ) - dao.upsertTrashItem(item) - return item + // Open temp blob stream and commit by rename after write completed. + // zh-CN: 打开临时 blob 流, 写入完成后通过 rename 提交. + val (tempRel, rawOut) = blobs.openTrashBlobTempOutputStream(trashId) + + // Commit point marker for cross-resource operation (FS + DB). + // zh-CN: 跨资源操作 (文件系统 + 数据库) 的提交点标记, 用于避免在源文件已删除后错误回滚导致数据丢失. + var operationCommitted = false + + // Track whether temp blob has been committed. + // zh-CN: 标记临时 blob 是否已提交. + var blobCommitted = false + + // Final relPath after commit. + // zh-CN: 提交后的最终 relPath. + var finalRel: String? = null + + try { + operationController.throwIfCancelled() + + rawOut.use { out -> + val digestOut = DigestOutputStream(out, digest) + val countingOut = CountingOutputStream(digestOut) + + if (isDir) { + // Zip directory to stream to avoid large memory usage. + // zh-CN: 将目录以 zip 流写入, 避免大内存占用. + zipDirectoryToStream( + dir = src, + out = countingOut, + operationController = operationController, + fileCallback = fileCallback, + onBytesProcessed = { n -> reportProcessed(n) }, + ) + } else { + // Copy file to stream to avoid large memory usage. + // zh-CN: 将文件以流复制写入, 避免大内存占用. + fileCallback?.onFile(src) + copyFileToStream( + src = src, + out = countingOut, + operationController = operationController, + onBytesProcessed = { n -> reportProcessed(n) }, + ) + } + + countingOut.flush() + sizeBytes = countingOut.count + } + + operationController.throwIfCancelled() + + // Commit temp blob into final blob name. + // zh-CN: 将临时 blob 提交为最终 blob 文件名. + finalRel = blobs.commitTrashBlobTemp(tempRel, trashId) + blobCommitted = true + + val sha = sha256HexFromDigest(digest) + + val item = TrashEntities.TrashItem( + trashId = trashId, + originalPath = logicalPath, + fileId = null, + trashedAt = now, + isDirectory = isDir, + sizeBytes = sizeBytes.coerceAtLeast(0L), + sha256 = sha, + blobRelPath = finalRel, + ) + + // Persist DB record before deleting source to reduce risk of data loss. + // zh-CN: 先写入数据库记录再删除源文件, 降低 "源已删但记录未写入" 的数据丢失风险. + dao.upsertTrashItem(item) + + // Delete original after blob persisted and DB record persisted. + // zh-CN: 在 blob 落盘且数据库记录写入后删除源文件/目录. + val deleted = if (isDir) src.deleteRecursively() else src.delete() + if (!deleted && src.exists()) { + // Roll back if source deletion failed. + // zh-CN: 若源文件删除失败则回滚, 避免出现 "回收站有记录但源文件仍存在" 的不一致状态. + throw IllegalStateException("Failed to delete source after trash: $src") + } + + // Mark committed after DB persisted and source deleted successfully. + // zh-CN: 在数据库写入成功且源文件删除成功后标记为已提交, 避免后续异常触发破坏性回滚. + operationCommitted = true + + /* Some code which may cause any exception in the future. */ + + return item + } catch (t: Throwable) { + // Clean up partial data on failure or abort, but avoid rollback after commit. + // zh-CN: 失败或中止时清理残留数据, 但在已提交后避免回滚以防数据丢失. + @Suppress("KotlinConstantConditions") + if (!operationCommitted) { + runCatching { dao.deleteTrashItemsByIds(listOf(trashId)) } + + // Delete final blob if committed, otherwise delete temp blob. + // zh-CN: 若已提交则删除最终 blob, 否则删除临时 blob. + if (blobCommitted) { + finalRel?.let { runCatching { blobs.deleteBlobByRelPath(it) } } + } else { + runCatching { blobs.deleteBlobByRelPath(tempRel) } + } + } + throw t + } + } + + private fun copyFileToStream( + src: File, + out: OutputStream, + operationController: DialogUtils.OperationController, + onBytesProcessed: ((Int) -> Unit)?, + ) { + operationController.throwIfCancelled() + BufferedInputStream(FileInputStream(src)).use { input -> + val buffer = ByteArray(256 * 1024) + while (true) { + operationController.throwIfCancelled() + val n = input.read(buffer) + if (n <= 0) break + out.write(buffer, 0, n) + onBytesProcessed?.invoke(n) + } + } + } + + /** + * Permanently delete a trash item (remove from trash only). + * zh-CN: 彻底删除一个回收站条目 (仅从回收站移除). + */ + fun deleteTrashItem(item: TrashEntities.TrashItem) { + dao.deleteTrashItemsByIds(listOf(item.trashId)) + blobs.deleteBlobByRelPath(item.blobRelPath) + } + + /** + * Clear all trash items. + * zh-CN: 清空回收站. + */ + fun clearTrash() { + val items = dao.listTrashItemsDesc() + if (items.isEmpty()) { + return + } + + dao.deleteAllTrashItems() + items.forEach { blobs.deleteBlobByRelPath(it.blobRelPath) } } /** @@ -89,14 +293,31 @@ class TrashRepository(private val context: Context) { * - 若为目录, dest 必须是目录路径. * - 恢复成功后会删除回收站条目. */ - fun restoreTrashItemToPath(item: TrashEntities.TrashItem, dest: File) { + fun restoreTrashItemToPath(item: TrashEntities.TrashItem, dest: File, operationController: DialogUtils.OperationController) { + operationController.throwIfCancelled() + val blobFile = File(context.filesDir, item.blobRelPath) require(blobFile.exists()) { "Trash blob not exists: ${item.blobRelPath}" } + // Copy stream with cancellation checkpoints. + // zh-CN: 带取消检查点的流拷贝, 以便在大文件恢复过程中及时响应终止操作. + fun copyToCancellable(input: InputStream, output: OutputStream) { + val buffer = ByteArray(256 * 1024) + while (true) { + operationController.throwIfCancelled() + val read = input.read(buffer) + if (read < 0) break + output.write(buffer, 0, read) + } + output.flush() + } + if (item.isDirectory) { dest.mkdirs() ZipInputStream(FileInputStream(blobFile)).use { zis -> while (true) { + operationController.throwIfCancelled() + val entry = zis.nextEntry ?: break val outFile = File(dest, entry.name) @@ -107,14 +328,18 @@ class TrashRepository(private val context: Context) { } outFile.parentFile?.mkdirs() - outFile.outputStream().use { os -> zis.copyTo(os) } + outFile.outputStream().use { os -> + copyToCancellable(zis, os) + } zis.closeEntry() } } } else { dest.parentFile?.mkdirs() blobFile.inputStream().use { input -> - dest.outputStream().use { output -> input.copyTo(output) } + dest.outputStream().use { output -> + copyToCancellable(input, output) + } } } @@ -124,12 +349,83 @@ class TrashRepository(private val context: Context) { blobs.deleteBlobByRelPath(item.blobRelPath) } - private fun sha256Hex(bytes: ByteArray): String { - val digest = MessageDigest.getInstance("SHA-256").digest(bytes) - val sb = StringBuilder(digest.size * 2) - for (b in digest) { - sb.append(String.format(Locale.US, "%02x", b)) + private fun zipDirectoryToStream( + dir: File, + out: OutputStream, + operationController: DialogUtils.OperationController, + fileCallback: ScriptOperations.FileProgressCallback?, + onBytesProcessed: ((Int) -> Unit)?, + ) { + ZipOutputStream(out).use { zos -> + val base = dir.absolutePath.trimEnd(File.separatorChar) + File.separator + + fun addFile(f: File) { + operationController.throwIfCancelled() + val abs = f.absolutePath + val relPath = abs.removePrefix(base).replace(File.separatorChar, '/') + + if (f.isDirectory) { + val children = f.listFiles() + if (children.isNullOrEmpty()) { + // Explicit directory entry for empty folder. + // zh-CN: 对空目录写入显式条目. + val entryName = if (relPath.endsWith("/")) relPath else "$relPath/" + zos.putNextEntry(ZipEntry(entryName)) + zos.closeEntry() + } else { + for (c in children) addFile(c) + } + return + } + + fileCallback?.onFile(f) + + zos.putNextEntry(ZipEntry(relPath)) + BufferedInputStream(FileInputStream(f)).use { input -> + val buffer = ByteArray(256 * 1024) + while (true) { + operationController.throwIfCancelled() + val n = input.read(buffer) + if (n <= 0) break + zos.write(buffer, 0, n) + onBytesProcessed?.invoke(n) + } + } + operationController.throwIfCancelled() + zos.closeEntry() + } + + addFile(dir) + zos.finish() } + } + + private fun sha256HexFromDigest(digest: MessageDigest): String { + val bytes = digest.digest() + val sb = StringBuilder(bytes.size * 2) + for (b in bytes) sb.append(String.format(Locale.US, "%02x", b)) return sb.toString() } + + /** + * Counting output stream wrapper. + * zh-CN: 计数输出流包装器. + */ + private class CountingOutputStream(private val delegate: OutputStream) : OutputStream() { + var count: Long = 0L + private set + + override fun write(b: Int) { + delegate.write(b) + count += 1L + } + + override fun write(b: ByteArray, off: Int, len: Int) { + delegate.write(b, off, len) + count += len.toLong() + } + + override fun flush() = delegate.flush() + override fun close() = delegate.close() + } } diff --git a/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt b/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt index b10ff518..dd62e8b4 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/TrashRestoreController.kt @@ -2,14 +2,22 @@ package org.autojs.autojs.storage.history import android.annotation.SuppressLint import android.content.Context +import android.text.TextUtils +import android.view.Gravity import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import org.autojs.autojs.app.DialogUtils -import org.autojs.autojs.app.DialogUtils.setActionButtonText +import org.autojs.autojs.model.explorer.ExplorerDirPage +import org.autojs.autojs.model.explorer.Explorers +import org.autojs.autojs.model.script.ScriptFile import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils +import org.autojs.autojs.util.DialogUtils.OperationAbortedException +import org.autojs.autojs.util.DialogUtils.OperationController +import org.autojs.autojs.util.DialogUtils.ProgressDialogSession +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.setActionButtonText import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R import java.io.File @@ -20,67 +28,108 @@ import java.util.regex.Pattern * zh-CN: 回收站条目的恢复流程控制器. * * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. */ +@SuppressLint("CheckResult") class TrashRestoreController(private val context: Context) { - fun startRestoreFlow(item: TrashEntities.TrashItem) { + fun startRestoreFlow( + item: TrashEntities.TrashItem, + optionMenuItems: List = emptyList(), + onFinished: (() -> Unit)? = null, + ) { val originalPath = item.originalPath val originalName = File(originalPath).name val selected = intArrayOf(0) + fun generateContent(context: Context, isUserChoice: Boolean): String { + val pathContent = if (isUserChoice) { + "[ ${context.getString(R.string.text_to_be_chosen)} ]" + } else originalPath.removeSuffix(originalName) + + return listOf( + if (item.isDirectory) { + context.getString(R.string.text_folder_colon_value, originalName) + } else { + context.getString(R.string.text_file_colon_value, originalName) + }, + context.getString(R.string.text_path_colon_value, pathContent), + ).joinToString("\n") + } + DialogUtils.buildAndShowAdaptive { MaterialDialog.Builder(context) .title(R.string.text_choose_restore_path) - .content(context.getString(R.string.text_path_colon_value, originalPath)) + .options(optionMenuItems) + .content(generateContent(context, false)) .items( context.getString(R.string.text_restore_to_original_path), context.getString(R.string.text_restore_to_specified_path), ) .itemsCallbackSingleChoice(0) { dialog, _, which, _ -> selected[0] = which - - // Update content and positive button text dynamically. - // zh-CN: 动态更新内容与确认按钮文本. when (which) { 0 -> { - dialog.setContent(context.getString(R.string.text_path_colon_value, originalPath)) + dialog.setContent(generateContent(context, isUserChoice = false)) dialog.setActionButtonText(DialogAction.POSITIVE, context.getString(R.string.dialog_button_confirm)) } else -> { - dialog.setContent(context.getString(R.string.text_file_name_colon_value, originalName)) + dialog.setContent(generateContent(context, isUserChoice = true)) dialog.setActionButtonText(DialogAction.POSITIVE, context.getString(R.string.dialog_button_choose_path)) } } true } + .alwaysCallSingleChoiceCallback() .choiceWidgetThemeColor() .negativeText(R.string.dialog_button_cancel) .negativeColorRes(R.color.dialog_button_default) + .onNegative { d, _ -> d.dismiss() } .positiveText(R.string.dialog_button_confirm) .positiveColorRes(R.color.dialog_button_attraction) - .onPositive { _, _ -> + .onPositive { d, _ -> when (selected[0]) { - 0 -> restoreToOriginalPathWithConflictHandling(item) - else -> chooseDirectoryThenRestore(item, originalName) + 0 -> restoreToOriginalPathWithConflictHandling( + item = item, + onRestore = { d.dismiss() }, + onFinished = onFinished, + ) + else -> chooseDirectoryThenRestore( + item = item, + fileName = originalName, + onRestore = { d.dismiss() }, + onFinished = onFinished, + ) } } .cancelable(true) + .autoDismiss(false) .build() + .apply { + contentView?.apply { + setLineSpacing(0f, 1.2f) + setLines(3) + minLines = 3 + maxLines = 9 + ellipsize = TextUtils.TruncateAt.MIDDLE + gravity = Gravity.CENTER_VERTICAL + } + } } } - private fun restoreToOriginalPathWithConflictHandling(item: TrashEntities.TrashItem) { + private fun restoreToOriginalPathWithConflictHandling(item: TrashEntities.TrashItem, onRestore: (() -> Unit)? = null, onFinished: (() -> Unit)? = null) { val dest = File(item.originalPath) val parent = dest.parentFile ?: run { ViewUtils.showToast(context, context.getString(R.string.error_invalid_path, item.originalPath), true) return } - restoreWithConflictHandling(item, parent, dest.name) + restoreWithConflictHandling(item, parent, dest.name, onRestore, onFinished) } @SuppressLint("CheckResult") - private fun chooseDirectoryThenRestore(item: TrashEntities.TrashItem, fileName: String) { + private fun chooseDirectoryThenRestore(item: TrashEntities.TrashItem, fileName: String, onRestore: (() -> Unit)? = null, onFinished: (() -> Unit)? = null) { FileChooserDialogBuilder(context) .title(R.string.dialog_button_choose_path) .dir(INTERNAL_STORAGE_ROOT) @@ -89,18 +138,18 @@ class TrashRestoreController(private val context: Context) { .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ dir -> - restoreWithConflictHandling(item, File(dir.path), fileName) + restoreWithConflictHandling(item, File(dir.path), fileName, onRestore, onFinished) }, { e -> e.printStackTrace() ViewUtils.showToast(context, e.message, true) }) } - private fun restoreWithConflictHandling(item: TrashEntities.TrashItem, destDir: File, fileName: String) { + private fun restoreWithConflictHandling(item: TrashEntities.TrashItem, destDir: File, fileName: String, onRestore: (() -> Unit)? = null, onFinished: (() -> Unit)? = null) { val dest = File(destDir, fileName) if (!dest.exists()) { - restoreNow(item, dest) + restoreNow(item, dest, onRestore, onFinished) return } @@ -116,34 +165,99 @@ class TrashRestoreController(private val context: Context) { .neutralColorRes(R.color.dialog_button_hint) .onNeutral { _, _ -> val newName = generateNextIndexedName(destDir, fileName) - restoreNow(item, File(destDir, newName)) + restoreNow(item, File(destDir, newName), onRestore, onFinished) } .positiveText(R.string.text_overwrite) .positiveColorRes(R.color.dialog_button_caution) .onPositive { _, _ -> - restoreNow(item, dest) + restoreNow(item, dest, onRestore, onFinished) } .cancelable(true) .build() } } - private fun restoreNow(item: TrashEntities.TrashItem, dest: File) { + private fun restoreNow(item: TrashEntities.TrashItem, dest: File, onRestore: (() -> Unit)? = null, onFinished: (() -> Unit)? = null) { + val destExistsBeforeRestore = dest.exists() Schedulers.io().scheduleDirect { runCatching { - TrashRepository(context.applicationContext).restoreTrashItemToPath(item, dest) + AndroidSchedulers.mainThread().scheduleDirect { + onRestore?.invoke() + } + + withControllableProgressDialog { operationController -> + TrashRepository(context.applicationContext).restoreTrashItemToPath(item, dest, operationController) + + // Notify Explorer to refresh the parent directory after restore. + // zh-CN: 恢复成功后通知 Explorer 刷新目标父目录. + notifyExplorerParentDirChanged(dest) + } + AndroidSchedulers.mainThread().scheduleDirect { ViewUtils.showToast(context, context.getString(R.string.text_done), true) + onFinished?.invoke() } - }.onFailure { - it.printStackTrace() - AndroidSchedulers.mainThread().scheduleDirect { - ViewUtils.showToast(context, it.message, true) + }.onFailure { e -> + when (e) { + is OperationAbortedException -> { + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(context, context.getString(R.string.text_operation_aborted), false) + } + if (!destExistsBeforeRestore && dest.exists()) { + dest.deleteRecursively() + } + } + else -> { + e.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(context, e.message, true) + } + } } } } } + @Throws(OperationAbortedException::class) + private fun withControllableProgressDialog(operation: (OperationController) -> Unit) { + val controller = OperationController() + val session = ProgressDialogSession(controller) + try { + val builder = MaterialDialog.Builder(context) + .title(R.string.text_restore) + .content(R.string.text_restoring) + .negativeText(R.string.dialog_button_abort) + .negativeColorRes(R.color.dialog_button_caution) + .onNegative { d, _ -> + controller.cancel() + } + .progress(true, 0) + .progressIndeterminateStyle(true) + .cancelable(false) + .canceledOnTouchOutside(false) + + session.scheduleShow(builder) + + controller.throwIfCancelled() + operation(controller) + controller.throwIfCancelled() + } catch (e: OperationAbortedException) { + throw e + } catch (e: Throwable) { + e.printStackTrace() + } finally { + session.dismissSafely() + } + } + + private fun notifyExplorerParentDirChanged(dest: File) { + runCatching { + val parent = dest.parentFile ?: return + val page = ExplorerDirPage(ScriptFile(parent.path), null) + Explorers.workspace().notifyChildrenChanged(page) + } + } + /** * Generate next indexed name by scanning existing "-n" siblings and taking the first missing n from 1. * zh-CN: 通过扫描同级目录中已有的 "-n" 名称, 从 1 开始取第一个缺失的 n 作为新后缀. diff --git a/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt b/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt index 61e69443..b004e405 100644 --- a/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt +++ b/app/src/main/java/org/autojs/autojs/storage/history/VersionHistoryController.kt @@ -1,15 +1,21 @@ package org.autojs.autojs.storage.history +import android.app.Activity import android.content.Context import android.net.Uri import com.afollestad.materialdialogs.MaterialDialog import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import org.autojs.autojs.app.DialogUtils +import org.autojs.autojs.util.DialogUtils +import org.autojs.autojs.model.explorer.ExplorerDirPage +import org.autojs.autojs.model.explorer.Explorers +import org.autojs.autojs.model.script.ScriptFile +import org.autojs.autojs.model.script.Scripts import org.autojs.autojs.pio.PFiles import org.autojs.autojs.ui.common.NotAskAgainDialog -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.ui.edit.EditActivity +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.util.StringUtils import org.autojs.autojs.util.StringUtils.key import org.autojs.autojs.util.ViewUtils @@ -28,6 +34,8 @@ import java.util.Locale * zh-CN: 版本历史对话框控制器. * * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by SuperMonster003 as of Feb 6, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 7, 2026. */ class VersionHistoryController(private val context: Context) { @@ -46,8 +54,10 @@ class VersionHistoryController(private val context: Context) { return } loadAndShow( - logicalPath = logicalPath, + canonicalPath = logicalPath, + displayPath = runCatching { Uri.decode(logicalPath) }.getOrElse { logicalPath }, mode = Mode.RESTORE_TO_EDITOR, + showPreview = false, onRestoreToEditor = onRestoreToEditor, onRestoredUi = onRestoredUi, ) @@ -57,15 +67,17 @@ class VersionHistoryController(private val context: Context) { * Show version history for explorer (restore to disk, overwriting the file content). * zh-CN: 为资源管理器显示版本历史 (恢复并写回磁盘, 覆盖文件内容). */ - fun showForFilePath(path: String) { + fun showForFilePath(path: String, displayPath: String) { val normalized = path.trimEnd('/') if (!normalized.startsWith(INTERNAL_STORAGE_ROOT)) { showNoHistoryDialog() return } loadAndShow( - logicalPath = normalized, + canonicalPath = normalized, + displayPath = displayPath, mode = Mode.RESTORE_TO_DISK, + showPreview = true, onRestoreToEditor = null, onRestoredUi = null, ) @@ -84,8 +96,10 @@ class VersionHistoryController(private val context: Context) { } private fun loadAndShow( - logicalPath: String, + canonicalPath: String, + displayPath: String, mode: Mode, + showPreview: Boolean = false, onRestoreToEditor: ((String) -> Unit)?, onRestoredUi: (() -> Unit)?, ) { @@ -94,7 +108,7 @@ class VersionHistoryController(private val context: Context) { val appCtx = context.applicationContext val dao = HistoryDatabase.getInstance(appCtx).historyDao() - val fileEntry = dao.findFileByPath(logicalPath) + val fileEntry = dao.findFileByPath(canonicalPath) val fileId = fileEntry?.fileId val revs = if (fileId != null) { @@ -120,36 +134,77 @@ class VersionHistoryController(private val context: Context) { val selectedIndex = intArrayOf(0) + val optionDeleteSelected = MaterialDialog.OptionMenuItemSpec( + context.getString(R.string.text_delete) + ) { dialog -> + val chosen = revs.getOrNull(selectedIndex[0]) ?: return@OptionMenuItemSpec + confirmAndDeleteSelectedRevision( + parentDialog = dialog, + rev = chosen, + canonicalPath = canonicalPath, + displayPath = displayPath, + mode = mode, + onRestoreToEditor = onRestoreToEditor, + onRestoredUi = onRestoredUi, + ) + } + DialogUtils.buildAndShowAdaptive { MaterialDialog.Builder(context) - .title(R.string.text_version_history) + .title(File(displayPath).name) + .options(listOf(optionDeleteSelected)) .items(items) .itemsCallbackSingleChoice(0) { _, _, which, _ -> selectedIndex[0] = which true } + .alwaysCallSingleChoiceCallback() .choiceWidgetThemeColor() + .apply { + if (!showPreview) return@apply + neutralText(R.string.dialog_button_preview) + neutralColorRes(R.color.dialog_button_hint) + onNeutral { _, _ -> + val chosen = revs.getOrNull(selectedIndex[0]) ?: return@onNeutral + previewInReadOnlyEditor(chosen, displayPath) + } + } .negativeText(R.string.dialog_button_cancel) .negativeColorRes(R.color.dialog_button_default) - .positiveText(R.string.dialog_button_retrieve) + .onNegative { d, _ -> d.dismiss() } + .positiveText(R.string.dialog_button_restore) .positiveColorRes(R.color.dialog_button_attraction) - .onPositive { _, _ -> + .onPositive { d, _ -> val chosen = revs.getOrNull(selectedIndex[0]) ?: return@onPositive when (mode) { Mode.RESTORE_TO_EDITOR -> { showRestoreConfirmDialogForEditor { + d.dismiss() restoreToEditor(chosen, onRestoreToEditor, onRestoredUi) } } Mode.RESTORE_TO_DISK -> { - showRestoreConfirmDialogForDisk(logicalPath) { - restoreToDisk(chosen, logicalPath) + showRestoreConfirmDialogForDisk(canonicalPath) { + d.dismiss() + restoreToDisk(chosen, canonicalPath) } } } } .cancelable(true) + .autoDismiss(false) .build() + .apply { + titleView.setOnLongClickListener { + when { + File(displayPath).exists() -> { + Scripts.edit(this@VersionHistoryController.context, displayPath) + true + } + else -> false + } + } + } } } }.onFailure { @@ -161,6 +216,36 @@ class VersionHistoryController(private val context: Context) { } } + /** + * Preview selected revision in a read-only editor. + * zh-CN: 在只读编辑器中预览所选 revision 内容. + */ + private fun previewInReadOnlyEditor(rev: HistoryEntities.Revision, displayPath: String) { + val appCtx = context.applicationContext + Schedulers.io().scheduleDirect { + runCatching { + val bytes = HistoryRepository(appCtx).readRevisionBytes(rev) + val restored = decodeRevisionBytes(bytes, rev.encoding, rev.hadBom) + + val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val timeText = fmt.format(Date(rev.createdAt)) + + val name = "[ ${context.getString(R.string.text_preview)} ] ${File(displayPath).name} @ $timeText" + + AndroidSchedulers.mainThread().scheduleDirect { + val newTask = context !is Activity + EditActivity.viewContent(context, name, restored, newTask) + } + }.onFailure { + it.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(context, it.message, true) + } + } + } + } + + private fun restoreToEditor( rev: HistoryEntities.Revision, onRestoreToEditor: ((String) -> Unit)?, @@ -190,6 +275,11 @@ class VersionHistoryController(private val context: Context) { runCatching { val bytes = HistoryRepository(appCtx).readRevisionBytes(rev) writeBytesTransactional(File(targetPath), bytes) + + // Notify Explorer to refresh the parent directory after restoring to disk. + // zh-CN: 写回磁盘恢复成功后通知 Explorer 刷新目标父目录. + notifyExplorerParentDirChanged(targetPath) + AndroidSchedulers.mainThread().scheduleDirect { ViewUtils.showToast(context, context.getString(R.string.text_done), true) } @@ -202,6 +292,14 @@ class VersionHistoryController(private val context: Context) { } } + private fun notifyExplorerParentDirChanged(targetPath: String) { + runCatching { + val parent = File(targetPath).parentFile ?: return + val page = ExplorerDirPage(ScriptFile(parent.path), null) + Explorers.workspace().notifyChildrenChanged(page) + } + } + private fun writeBytesTransactional(dest: File, bytes: ByteArray) { dest.parentFile?.mkdirs() @@ -282,6 +380,72 @@ class VersionHistoryController(private val context: Context) { return MessageDigest.getInstance("SHA-256").digest(bytes) } + /** + * Confirm deleting selected revision. + * zh-CN: 确认删除所选 revision. + */ + private fun confirmAndDeleteSelectedRevision( + parentDialog: MaterialDialog, + rev: HistoryEntities.Revision, + canonicalPath: String, + displayPath: String, + mode: Mode, + onRestoreToEditor: ((String) -> Unit)?, + onRestoredUi: (() -> Unit)?, + ) { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(context) + .title(R.string.text_prompt) + .content(R.string.text_delete_revision_confirm) + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { _, _ -> + parentDialog.dismiss() + deleteRevisionAndReloadDialog(rev, canonicalPath, displayPath, mode, onRestoreToEditor, onRestoredUi) + } + .cancelable(true) + .build() + } + } + + /** + * Delete revision and reload dialog. + * zh-CN: 删除 revision 并重新加载对话框. + */ + private fun deleteRevisionAndReloadDialog( + rev: HistoryEntities.Revision, + canonicalPath: String, + displayPath: String, + mode: Mode, + onRestoreToEditor: ((String) -> Unit)?, + onRestoredUi: (() -> Unit)?, + ) { + val appCtx = context.applicationContext + Schedulers.io().scheduleDirect { + runCatching { + HistoryRepository(appCtx).deleteRevision(rev) + }.onSuccess { + AndroidSchedulers.mainThread().scheduleDirect { + loadAndShow( + canonicalPath = canonicalPath, + displayPath = displayPath, + mode = mode, + showPreview = true, + onRestoreToEditor = onRestoreToEditor, + onRestoredUi = onRestoredUi, + ) + } + }.onFailure { + it.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(context, it.message, true) + } + } + } + } + private enum class Mode { RESTORE_TO_EDITOR, RESTORE_TO_DISK, diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorHistoryDao.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorHistoryDao.kt index d6627a65..04cb9a07 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorHistoryDao.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorHistoryDao.kt @@ -48,7 +48,7 @@ interface ColorHistoryDao { suspend fun hasDataByLibraryId(libraryId: Long): Boolean @Delete - suspend fun delete(vararg histories: ColorHistory) + suspend fun delete(vararg history: ColorHistory) @Query("DELETE FROM color_history") suspend fun deleteAll() diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ThemeColorHistoriesItemAdapter.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorHistoryItemAdapter.kt similarity index 100% rename from app/src/main/java/org/autojs/autojs/theme/app/ThemeColorHistoriesItemAdapter.kt rename to app/src/main/java/org/autojs/autojs/theme/app/ColorHistoryItemAdapter.kt diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorInfoDialogManager.kt index 6c56283c..e2af6391 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorInfoDialogManager.kt @@ -13,9 +13,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.autojs.autojs.app.DialogUtils.showAdaptive -import org.autojs.autojs.util.MaterialDialogUtils.makeTextCopyable -import org.autojs.autojs.util.MaterialDialogUtils.setCopyableText +import org.autojs.autojs.util.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.makeTextCopyable +import org.autojs.autojs.util.DialogUtils.setCopyableText import org.autojs.autojs.runtime.api.augment.colors.Colors import org.autojs.autojs.util.ColorUtils import org.autojs.autojs6.R diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorItemAdapter.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorItemAdapter.kt index 43a817b4..5909b0e0 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorItemAdapter.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorItemAdapter.kt @@ -10,7 +10,7 @@ import org.autojs.autojs.theme.ThemeChangeNotifier import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.theme.app.ColorLibrariesActivity.Companion.PresetColorItem import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.SELECT_NONE -import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.saveDatabaseForColorHistories +import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.saveDatabaseForColorHistory import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.savePrefsForLegacy import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.savePrefsForLibraries import org.autojs.autojs6.R @@ -68,7 +68,7 @@ class ColorItemAdapter( private fun savePrefsAndDatabase(context: Context, item: PresetColorItem) { savePrefsForLibraries(item) savePrefsForLegacy(context, item) - saveDatabaseForColorHistories( + saveDatabaseForColorHistory( applicationContext = context.applicationContext, libraryId = item.libraryId, itemId = item.itemId, diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorItemsActivity.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorItemsActivity.kt index 866cb168..4b846a4f 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorItemsActivity.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorItemsActivity.kt @@ -289,8 +289,8 @@ class ColorItemsActivity : ColorSelectBaseActivity() { locateCurrentThemeColor() true } - R.id.action_show_histories -> { - showColorHistories(mLibrary.id) { selectedHistoryItem -> + R.id.action_show_history -> { + showColorHistory(mLibrary.id) { selectedHistoryItem -> when (val targetIndex = mAdapter.items.indexOfFirst { getColor(it.colorRes) == selectedHistoryItem.colorInt }) { -1 -> ViewUtils.showToast(this, R.string.error_failed_to_apply_current_color_history, true) else -> { diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorLibrariesActivity.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorLibrariesActivity.kt index 427e2cd9..11136ef5 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorLibrariesActivity.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorLibrariesActivity.kt @@ -126,22 +126,22 @@ class ColorLibrariesActivity : ColorSelectBaseActivity() { showColorPicker() true } - R.id.action_show_all_histories -> { - showColorHistories { selectedHistoryItem -> + R.id.action_show_all_history_entries -> { + showColorHistory { selectedHistoryItem -> val identifier = selectedHistoryItem.colorLibraryIdentifier ?: run { ViewUtils.showToast(this@ColorLibrariesActivity, R.string.error_failed_to_apply_current_color_history, true) - return@showColorHistories + return@showColorHistory } if (identifier == getString(R.string.color_library_identifier_palette)) { onColorPickerConfirmed(selectedHistoryItem.colorInt) - return@showColorHistories + return@showColorHistory } presetColorLibraries.find { getString(it.identifierRes) == identifier }?.let { library -> library.colors.firstOrNull { getColor(it.colorRes) == selectedHistoryItem.colorInt }?.let { item -> savePrefsForLibraries(item) savePrefsForLegacy(this, item) - saveDatabaseForColorHistories( + saveDatabaseForColorHistory( applicationContext = applicationContext, libraryId = item.libraryId, itemId = item.itemId, diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorSelectBaseActivity.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorSelectBaseActivity.kt index affb6177..347bd80f 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorSelectBaseActivity.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorSelectBaseActivity.kt @@ -264,7 +264,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { .setColor(initialColor ?: customColor) .setOldColorPanelOnClickListener { v, dialog -> showColorDetails(v.tag as? Int, parentDialog = dialog) } .setNewColorPanelOnClickListener { v, dialog -> showColorDetails(v.tag as? Int, parentDialog = dialog) } - .setColorHistoriesHandler { showColorPickerHistories(it) } + .setColorHistoryHandler { showColorPickerHistory(it) } .create() .setColorPickerDialogListener { _, color: Int -> onColorPickerConfirmed(color) @@ -276,7 +276,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { savePrefsForLegacyPalette(color) savePrefsForLibraries(COLOR_LIBRARY_ID_PALETTE, 0) - saveDatabaseForPaletteHistories(lifecycleScope, applicationContext, color) + saveDatabaseForPaletteHistory(lifecycleScope, applicationContext, color) isForciblyEnableAppBarColorTransition = true @@ -284,16 +284,16 @@ abstract class ColorSelectBaseActivity : BaseActivity() { ThemeChangeNotifier.notifyThemeChanged() } - private fun showColorPickerHistories(colorPickerDialog: ColorPickerDialog) { + private fun showColorPickerHistory(colorPickerDialog: ColorPickerDialog) { lifecycleScope.launch { - var historiesDialog: MaterialDialog? = null + var historyDialog: MaterialDialog? = null val dao = withContext(Dispatchers.IO) { ColorHistoryDatabase.getInstance(applicationContext).paletteHistoryDao() } when { withContext(Dispatchers.IO) { dao.hasData() } -> { val adapter = PaletteHistoryItemAdapter(emptyList()) { selectedColor -> - historiesDialog?.dismiss() + historyDialog?.dismiss() colorPickerDialog.onHistorySelected(selectedColor) } launch(Dispatchers.Main) { @@ -303,15 +303,15 @@ abstract class ColorSelectBaseActivity : BaseActivity() { it.layoutManager = LinearLayoutManager(context) it.addItemDecoration(DividerItemDecoration(context, VERTICAL)) } - historiesDialog = MaterialDialog.Builder(context) - .title(R.string.text_histories) + historyDialog = MaterialDialog.Builder(context) + .title(R.string.text_history) .customView(recyclerView, false) .neutralText(R.string.dialog_button_clear_items) .neutralColorRes(R.color.dialog_button_warn) .onNeutral { parentDialog, _ -> MaterialDialog.Builder(context) .title(R.string.text_prompt) - .content(getString(R.string.text_confirm_to_clear_histories_of, getString(R.string.text_color_palette))) + .content(getString(R.string.text_confirm_to_clear_the_history_of, getString(R.string.text_color_palette))) .negativeText(R.string.dialog_button_cancel) .negativeColorRes(R.color.dialog_button_default) .positiveText(R.string.dialog_button_confirm) @@ -322,7 +322,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { withContext(Dispatchers.Main) { adapter.updateData(emptyList()) parentDialog.dismiss() - ViewUtils.showToast(context, R.string.text_all_histories_cleared, true) + ViewUtils.showToast(context, R.string.text_history_has_been_cleared, true) } } } @@ -334,17 +334,17 @@ abstract class ColorSelectBaseActivity : BaseActivity() { .autoDismiss(false) .show() } - val histories = withContext(Dispatchers.IO) { + val history = withContext(Dispatchers.IO) { dao.getAll().sortedByDescending { it.lastUsedTime } } launch(Dispatchers.Main) { - adapter.updateData(histories) + adapter.updateData(history) } } else -> withContext(Dispatchers.Main) { MaterialDialog.Builder(this@ColorSelectBaseActivity) - .title(R.string.text_histories) - .content(R.string.text_no_histories) + .title(R.string.text_history) + .content(R.string.text_no_history) .positiveText(R.string.dialog_button_dismiss) .positiveColorRes(R.color.dialog_button_default) .show() @@ -353,12 +353,12 @@ abstract class ColorSelectBaseActivity : BaseActivity() { } } - protected fun showColorHistories(libraryId: Int? = null, onHistorySelected: (selectedHistoryItem: ColorHistoryItem) -> Unit) { + protected fun showColorHistory(libraryId: Int? = null, onHistorySelected: (selectedHistoryItem: ColorHistoryItem) -> Unit) { - val isShowAllHistories = libraryId == null + val isShowAllHistoryEntries = libraryId == null lifecycleScope.launch { - var historiesDialog: MaterialDialog? = null + var historyDialog: MaterialDialog? = null var paletteHistoryDao: PaletteHistoryDao? = null val colorHistoryDao = withContext(Dispatchers.IO) { ColorHistoryDatabase.getInstance(applicationContext).colorHistoryDao() @@ -372,8 +372,8 @@ abstract class ColorSelectBaseActivity : BaseActivity() { if (!hasData) { withContext(Dispatchers.Main) { MaterialDialog.Builder(this@ColorSelectBaseActivity) - .title(R.string.text_histories) - .content(R.string.text_no_histories) + .title(R.string.text_history) + .content(R.string.text_no_history) .positiveText(R.string.dialog_button_dismiss) .positiveColorRes(R.color.dialog_button_default) .show() @@ -381,7 +381,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { return@launch } val adapter = ColorHistoryItemAdapter(emptyList()) { selectedHistoryItem -> - historiesDialog?.dismiss() + historyDialog?.dismiss() onHistorySelected(selectedHistoryItem) } launch(Dispatchers.Main) { @@ -391,8 +391,8 @@ abstract class ColorSelectBaseActivity : BaseActivity() { it.layoutManager = LinearLayoutManager(context) it.addItemDecoration(DividerItemDecoration(context, VERTICAL)) } - historiesDialog = MaterialDialog.Builder(context) - .title(R.string.text_histories) + historyDialog = MaterialDialog.Builder(context) + .title(R.string.text_history) .customView(recyclerView, false) .neutralText(R.string.dialog_button_clear_items) .neutralColorRes(R.color.dialog_button_warn) @@ -401,12 +401,12 @@ abstract class ColorSelectBaseActivity : BaseActivity() { .title(R.string.text_prompt) .apply { when { - isShowAllHistories -> { - content(getString(R.string.text_confirm_to_clear_histories_of_all_color_libraries)) + isShowAllHistoryEntries -> { + content(getString(R.string.text_confirm_to_clear_the_history_of_all_color_libraries)) } else -> presetColorLibraries.find { it.id == libraryId }?.let { - content(getString(R.string.text_confirm_to_clear_histories_of, getString(it.nameRes))) - } ?: content(getString(R.string.text_confirm_to_clear_all_histories)) + content(getString(R.string.text_confirm_to_clear_the_history_of, getString(it.nameRes))) + } ?: content(getString(R.string.text_confirm_to_clear_the_history)) } } .negativeText(R.string.dialog_button_cancel) @@ -415,7 +415,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { .positiveColorRes(R.color.dialog_button_caution) .onPositive { _, _ -> lifecycleScope.launch(Dispatchers.IO) { - if (isShowAllHistories) { + if (isShowAllHistoryEntries) { if (paletteHistoryDao == null) { paletteHistoryDao = withContext(Dispatchers.IO) { ColorHistoryDatabase.getInstance(applicationContext).paletteHistoryDao() @@ -429,7 +429,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { withContext(Dispatchers.Main) { adapter.updateData(emptyList()) parentDialog.dismiss() - ViewUtils.showToast(context, R.string.text_all_histories_cleared, true) + ViewUtils.showToast(context, R.string.text_history_has_been_cleared, true) } } } @@ -442,14 +442,14 @@ abstract class ColorSelectBaseActivity : BaseActivity() { .show() } - val colorHistories = withContext(Dispatchers.IO) { + val colorHistory = withContext(Dispatchers.IO) { when { - isShowAllHistories -> colorHistoryDao.getAll() + isShowAllHistoryEntries -> colorHistoryDao.getAll() else -> colorHistoryDao.getAllByLibraryId(libraryId.toLong()) } } - val historyItems = colorHistories.mapNotNull { history -> + val historyItems = colorHistory.mapNotNull { history -> var colorInt: Int? = null var colorName: String? = null var colorLibraryIdentifier: String? = null @@ -460,7 +460,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { presetColorLibrary.colors.find { it.itemId == history.itemId }?.let { item -> colorInt = getColor(item.colorRes) colorName = getString(item.nameRes) - if (isShowAllHistories) { + if (isShowAllHistoryEntries) { colorLibraryIdentifier = getString(presetColorLibrary.identifierRes) } } @@ -480,7 +480,7 @@ abstract class ColorSelectBaseActivity : BaseActivity() { return@mapNotNull null }.toMutableList() - if (isShowAllHistories) { + if (isShowAllHistoryEntries) { val paletteIdentifier = getString(R.string.color_library_identifier_palette) if (paletteHistoryDao == null) { paletteHistoryDao = withContext(Dispatchers.IO) { @@ -778,22 +778,22 @@ abstract class ColorSelectBaseActivity : BaseActivity() { } @JvmStatic - fun saveDatabaseForPaletteHistories(dispatcherScope: CoroutineScope? = null, applicationContext: Context, color: Int) { - saveHistories(dispatcherScope) { + fun saveDatabaseForPaletteHistory(dispatcherScope: CoroutineScope? = null, applicationContext: Context, color: Int) { + saveHistory(dispatcherScope) { var itemId by Delegates.notNull() - savePaletteHistories(applicationContext, color).also { itemId = it } - saveColorHistories(applicationContext, COLOR_LIBRARY_ID_PALETTE, itemId) + savePaletteHistory(applicationContext, color).also { itemId = it } + saveColorHistory(applicationContext, COLOR_LIBRARY_ID_PALETTE, itemId) } } @JvmStatic - fun saveDatabaseForColorHistories(dispatcherScope: CoroutineScope? = null, applicationContext: Context, libraryId: Int, itemId: Int) { - saveHistories(dispatcherScope) { - saveColorHistories(applicationContext, libraryId, itemId) + fun saveDatabaseForColorHistory(dispatcherScope: CoroutineScope? = null, applicationContext: Context, libraryId: Int, itemId: Int) { + saveHistory(dispatcherScope) { + saveColorHistory(applicationContext, libraryId, itemId) } } - private fun saveHistories( + private fun saveHistory( dispatcherScope: CoroutineScope? = null, saveJob: suspend CoroutineScope.() -> Unit, ) { @@ -803,13 +803,13 @@ abstract class ColorSelectBaseActivity : BaseActivity() { } } - private suspend fun savePaletteHistories(applicationContext: Context, color: Int): Int { + private suspend fun savePaletteHistory(applicationContext: Context, color: Int): Int { val dao = ColorHistoryDatabase.getInstance(applicationContext).paletteHistoryDao() val colorInfo = ColorInfo(ColorUtils.toHex(color, 6)) return dao.upsert(PaletteHistory(colorInfo = colorInfo).apply { setLastUsedTimeCurrent() }).toInt() } - private suspend fun saveColorHistories(applicationContext: Context, libraryId: Int, itemId: Int) { + private suspend fun saveColorHistory(applicationContext: Context, libraryId: Int, itemId: Int) { val dao = ColorHistoryDatabase.getInstance(applicationContext).colorHistoryDao() dao.upsert(ColorHistory(libraryId = libraryId, itemId = itemId).apply { setLastUsedTimeCurrent() }) } diff --git a/app/src/main/java/org/autojs/autojs/theme/app/ColorSettingRecyclerView.kt b/app/src/main/java/org/autojs/autojs/theme/app/ColorSettingRecyclerView.kt index d4ca3198..ba58b4b9 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/ColorSettingRecyclerView.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/ColorSettingRecyclerView.kt @@ -31,8 +31,8 @@ import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.SELECT_NONE import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.colorItemsLegacy import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.customColorPosition import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.defaultColorPosition -import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.saveDatabaseForColorHistories -import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.saveDatabaseForPaletteHistories +import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.saveDatabaseForColorHistory +import org.autojs.autojs.theme.app.ColorSelectBaseActivity.Companion.saveDatabaseForPaletteHistory import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R import org.autojs.autojs6.databinding.MtColorSettingRecyclerViewItemBinding @@ -70,7 +70,7 @@ class ColorSettingRecyclerView : ThemeColorRecyclerView { presetColorLibraries.find { it.isDefault }!!.colors.find { colorItem -> context.getColor(colorItem.colorRes) == context.getColor(R.color.theme_color_default) }?.let { - saveDatabaseForColorHistories( + saveDatabaseForColorHistory( applicationContext = context.applicationContext, libraryId = COLOR_LIBRARY_ID_DEFAULT, itemId = it.itemId, @@ -85,7 +85,7 @@ class ColorSettingRecyclerView : ThemeColorRecyclerView { presetColorLibraries.find { it.isMaterial }!!.colors.find { colorItem -> context.getColor(colorItem.colorRes) == c }?.let { - saveDatabaseForColorHistories( + saveDatabaseForColorHistory( applicationContext = context.applicationContext, libraryId = COLOR_LIBRARY_ID_MATERIAL, itemId = it.itemId, @@ -174,7 +174,7 @@ class ColorSettingRecyclerView : ThemeColorRecyclerView { val colorWithFullAlpha = color or Color.BLACK Pref.putInt(ColorSelectBaseActivity.KEY_CUSTOM_COLOR, colorWithFullAlpha) setSelectedPosition(customColorPosition) - saveDatabaseForPaletteHistories(applicationContext = context.applicationContext, color = colorWithFullAlpha) + saveDatabaseForPaletteHistory(applicationContext = context.applicationContext, color = colorWithFullAlpha) mOnItemClickListener?.onItemClick(v, customColorPosition) } .show((context as FragmentActivity).supportFragmentManager, "ColorPickerTagForColorSelect") diff --git a/app/src/main/java/org/autojs/autojs/theme/app/PaletteHistoryDao.kt b/app/src/main/java/org/autojs/autojs/theme/app/PaletteHistoryDao.kt index 20e5965f..63e477bc 100644 --- a/app/src/main/java/org/autojs/autojs/theme/app/PaletteHistoryDao.kt +++ b/app/src/main/java/org/autojs/autojs/theme/app/PaletteHistoryDao.kt @@ -38,7 +38,7 @@ interface PaletteHistoryDao { suspend fun hasData(): Boolean @Delete - suspend fun delete(vararg histories: PaletteHistory): Int + suspend fun delete(vararg historyItem: PaletteHistory): Int @Query("DELETE FROM palette_history") suspend fun deleteAll() diff --git a/app/src/main/java/org/autojs/autojs/theme/preference/MaterialListPreference.kt b/app/src/main/java/org/autojs/autojs/theme/preference/MaterialListPreference.kt index 17344aa5..926359a1 100644 --- a/app/src/main/java/org/autojs/autojs/theme/preference/MaterialListPreference.kt +++ b/app/src/main/java/org/autojs/autojs/theme/preference/MaterialListPreference.kt @@ -5,10 +5,10 @@ import android.os.Bundle import android.text.TextUtils import android.util.AttributeSet import com.afollestad.materialdialogs.MaterialDialog -import org.autojs.autojs.app.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.showAdaptive import org.autojs.autojs.core.pref.Pref -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.ui.common.NotAskAgainDialog import org.autojs.autojs6.R diff --git a/app/src/main/java/org/autojs/autojs/theme/preference/MaterialPreference.kt b/app/src/main/java/org/autojs/autojs/theme/preference/MaterialPreference.kt index 9ee6ab8c..704085db 100644 --- a/app/src/main/java/org/autojs/autojs/theme/preference/MaterialPreference.kt +++ b/app/src/main/java/org/autojs/autojs/theme/preference/MaterialPreference.kt @@ -4,7 +4,7 @@ import android.content.Context import android.content.res.TypedArray import android.util.AttributeSet import androidx.preference.PreferenceViewHolder -import org.autojs.autojs.app.DialogUtils +import org.autojs.autojs.util.DialogUtils import org.autojs.autojs6.R open class MaterialPreference : androidx.preference.Preference, LongClickablePreferenceLike { diff --git a/app/src/main/java/org/autojs/autojs/theme/preference/ThemeColorSwitchPreference.kt b/app/src/main/java/org/autojs/autojs/theme/preference/ThemeColorSwitchPreference.kt index 2bfe331d..44e2149f 100644 --- a/app/src/main/java/org/autojs/autojs/theme/preference/ThemeColorSwitchPreference.kt +++ b/app/src/main/java/org/autojs/autojs/theme/preference/ThemeColorSwitchPreference.kt @@ -10,7 +10,7 @@ import android.widget.Switch import androidx.appcompat.widget.SwitchCompat import androidx.preference.PreferenceViewHolder import androidx.preference.SwitchPreference -import org.autojs.autojs.app.DialogUtils +import org.autojs.autojs.util.DialogUtils import org.autojs.autojs.theme.ThemeColor import org.autojs.autojs.theme.ThemeColorHelper import org.autojs.autojs.theme.ThemeColorManager diff --git a/app/src/main/java/org/autojs/autojs/timing/ExactAlarmPermissionHelper.kt b/app/src/main/java/org/autojs/autojs/timing/ExactAlarmPermissionHelper.kt index b0364c42..f95e556a 100644 --- a/app/src/main/java/org/autojs/autojs/timing/ExactAlarmPermissionHelper.kt +++ b/app/src/main/java/org/autojs/autojs/timing/ExactAlarmPermissionHelper.kt @@ -7,8 +7,8 @@ import android.os.Build import android.provider.Settings import androidx.annotation.RequiresApi import androidx.core.net.toUri -import org.autojs.autojs.app.DialogUtils.showAdaptive -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.ui.common.NotAskAgainDialog import org.autojs.autojs.util.IntentUtils.startSafely import org.autojs.autojs.util.StringUtils.key diff --git a/app/src/main/java/org/autojs/autojs/ui/common/ScriptLoopDialog.java b/app/src/main/java/org/autojs/autojs/ui/common/ScriptLoopDialog.java index 68766a2a..3386e66c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/common/ScriptLoopDialog.java +++ b/app/src/main/java/org/autojs/autojs/ui/common/ScriptLoopDialog.java @@ -6,7 +6,7 @@ import android.view.Window; import android.widget.EditText; import android.widget.LinearLayout; import com.afollestad.materialdialogs.MaterialDialog; -import org.autojs.autojs.app.DialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.model.script.ScriptFile; import org.autojs.autojs.model.script.Scripts; import org.autojs.autojs.util.ViewUtils; diff --git a/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java b/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java index a5408d97..50da5b4c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java +++ b/app/src/main/java/org/autojs/autojs/ui/common/ScriptOperations.java @@ -4,13 +4,11 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.Intent; import android.os.Environment; -import android.os.SystemClock; import android.text.Editable; import android.text.InputType; import android.util.Log; import android.view.View; import android.widget.EditText; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.pm.ShortcutManagerCompat; @@ -21,7 +19,6 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.internal.functions.ObjectHelper; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; -import org.autojs.autojs.app.DialogUtils; import org.autojs.autojs.app.GlobalAppContext; import org.autojs.autojs.core.pref.Pref; import org.autojs.autojs.external.ScriptIntents; @@ -40,14 +37,19 @@ import org.autojs.autojs.pio.PFiles; import org.autojs.autojs.pio.UncheckedIOException; import org.autojs.autojs.project.ProjectConfig; import org.autojs.autojs.storage.file.TmpScriptFiles; +import org.autojs.autojs.storage.history.HistoryPrefs; +import org.autojs.autojs.storage.history.HistoryRepository; import org.autojs.autojs.storage.history.TrashRepository; import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder; import org.autojs.autojs.ui.shortcut.ShortcutCreateActivity; import org.autojs.autojs.ui.timing.TimedTaskSettingActivity; +import org.autojs.autojs.util.DialogUtils; +import org.autojs.autojs.util.DialogUtils.OperationAbortedException; +import org.autojs.autojs.util.DialogUtils.OperationController; +import org.autojs.autojs.util.DialogUtils.ProgressDialogSession; import org.autojs.autojs.util.EnvironmentUtils; import org.autojs.autojs.util.FileUtils; import org.autojs.autojs.util.IntentUtils; -import org.autojs.autojs.util.MaterialDialogUtils; import org.autojs.autojs.util.ShortcutUtils; import org.autojs.autojs.util.ViewUtils; import org.autojs.autojs.util.WorkingDirectoryUtils; @@ -65,44 +67,27 @@ import java.util.List; import java.util.Objects; import java.util.Set; import java.util.UUID; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static org.autojs.autojs.app.DialogUtils.fixCheckBoxGravity; import static org.autojs.autojs.model.explorer.ExplorerFileItem.isInSampleDir; +import static org.autojs.autojs.util.DialogUtils.fixCheckBoxGravity; import static org.autojs.autojs.util.FileUtils.TYPE.JAVASCRIPT; import static org.autojs.autojs.util.RhinoUtils.isMainThread; import static org.autojs.autojs.util.ThreadUtils.runOnMain; /** * Created by Stardust on Jul 31, 2017. - * Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 31, 2026. - * Modified by SuperMonster003 as of Feb 1, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 6, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. */ -@SuppressWarnings({"ResultOfMethodCallIgnored", "CodeBlock2Expr"}) +@SuppressWarnings({"ResultOfMethodCallIgnored", "CodeBlock2Expr", "unused"}) @SuppressLint("CheckResult") public class ScriptOperations { private static final String LOG_TAG = "ScriptOperations"; - // Minimum UI update interval for progress dialog, to reduce main-thread pressure. - // zh-CN: 进度对话框 UI 更新的最小间隔, 用于降低主线程压力. - private static final long PROGRESS_UPDATE_MIN_INTERVAL_MS = 50L; - - // Delay before showing progress dialog to avoid flashing for fast operations. - // zh-CN: 显示进度对话框前的延迟, 用于避免快速操作导致的闪现. - private static final long PROGRESS_DIALOG_SHOW_DELAY_MS = 200L; - - // Minimum duration to keep progress dialog visible once shown, to avoid flashing. - // zh-CN: 进度对话框一旦显示后的最短展示时长, 用于避免闪现. - private static final long PROGRESS_DIALOG_MIN_SHOW_MS = 300L; - - // Progress max for large files (totalBytes > Integer.MAX_VALUE), higher means smoother progress. - // zh-CN: 大文件 (totalBytes > Integer.MAX_VALUE) 的进度最大值, 值越大进度越细腻. - private static final int PROGRESS_MAX_LARGE = 10_000; - // Suffix for partial copy output file. // zh-CN: 复制过程的临时输出文件后缀. private static final String PARTIAL_COPY_SUFFIX = ".partial"; @@ -168,7 +153,7 @@ public class ScriptOperations { Scripts.edit(mContext, path); } } else { - showMessage(R.string.text_failed_to_create); + showMessage(R.string.error_failed_to_create); } } @@ -225,12 +210,12 @@ public class ScriptOperations { .positiveText(R.string.dialog_button_confirm) .positiveColorRes(R.color.dialog_button_attraction) .negativeText(R.string.dialog_button_back); - MaterialDialogUtils.choiceWidgetThemeColor(builderDefaultPrefix); + DialogUtils.choiceWidgetThemeColor(builderDefaultPrefix); MaterialDialog dialogDefaultPrefix = builderDefaultPrefix.build(); DialogUtils.showAdaptive(dialogDefaultPrefix); }) .autoDismiss(false); - MaterialDialogUtils.widgetThemeColor(builder); + DialogUtils.widgetThemeColor(builder); MaterialDialog dialog = builder.build(); dialogRef.set(dialog); @@ -327,7 +312,7 @@ public class ScriptOperations { showMessage(R.string.text_already_created); notifyFileCreated(new ScriptFile(newDir)); } else { - showMessage(R.string.text_failed_to_create); + showMessage(R.string.error_failed_to_create); } }); } @@ -367,7 +352,7 @@ public class ScriptOperations { input.onComplete(); }) .canceledOnTouchOutside(false); - MaterialDialogUtils.widgetThemeColor(builder); + DialogUtils.widgetThemeColor(builder); DialogUtils.showAdaptive(builder.build()); return input; } @@ -395,7 +380,6 @@ public class ScriptOperations { } ExplorerFileItem newItem = item.rename(newName); if (ObjectHelper.equals(newItem.toScriptFile(), item.toScriptFile())) { - showMessage(R.string.error_cannot_rename); throw new IOException(); } notifyFileChanged(item, newItem); @@ -434,12 +418,6 @@ public class ScriptOperations { // zh-CN: 用于中止处理与刷新. boolean destTouched = false; - // Timestamp/progress/content state for throttling UI updates. - // zh-CN: 用于节流 UI 更新的时间戳/进度/文本状态. - final long[] lastProgressUpdateTime = new long[]{0L}; - final long[] lastProgressValue = new long[]{-1L}; - final long[] lastContentUpdateTime = new long[]{0L}; - try { long totalBytes = computeTotalBytes(source); @@ -456,11 +434,11 @@ public class ScriptOperations { totalBytes, (copied, total) -> { controller.throwIfCancelled(); - session.setProgressThrottled(copied, totalBytes, lastProgressUpdateTime, lastProgressValue); + session.setProgressThrottled(copied, totalBytes); }, (currentFile) -> { controller.throwIfCancelled(); - session.setContentThrottled(toContentList(source, dest, currentFile), lastContentUpdateTime); + session.setContentThrottled(toContentList(source, dest, currentFile)); }, controller ); @@ -475,11 +453,11 @@ public class ScriptOperations { totalBytes, (copied, total) -> { controller.throwIfCancelled(); - session.setProgressThrottled(copied, totalBytes, lastProgressUpdateTime, lastProgressValue); + session.setProgressThrottled(copied, totalBytes); }, (currentFile) -> { controller.throwIfCancelled(); - session.setContentThrottled(toContentList(source, dest, currentFile), lastContentUpdateTime); + session.setContentThrottled(toContentList(source, dest, currentFile)); }, controller ); @@ -494,7 +472,7 @@ public class ScriptOperations { // Cleanup and refresh when aborted. // zh-CN: 中止时清理并刷新. if (destTouched) { - notifyChildrenChangedAtItsParent(new ScriptFile(dest.getPath())); + notifyChildrenChangedAtItsParent(new ScriptFile(dest)); if (isMove) { notifyChildrenChangedAtItsParent(source); } @@ -529,18 +507,15 @@ public class ScriptOperations { ? isMove ? R.string.text_move_folder : R.string.text_copy_folder : isMove ? R.string.text_move_file : R.string.text_copy_file; + int contentRes = isMove ? R.string.text_moving : R.string.text_copying; + MaterialDialog.Builder builder = new MaterialDialog.Builder(mContext) .title(titleRes) - .content(R.string.ellipsis_six) + .content(contentRes) .negativeText(R.string.dialog_button_abort) .negativeColorRes(R.color.dialog_button_caution) .onNegative((dialog, which) -> { controller.cancel(); - try { - dialog.setContent(mContext.getString(R.string.text_aborting)); - } catch (Throwable ignored) { - /* Ignored. */ - } }) .cancelable(false) .canceledOnTouchOutside(false); @@ -552,7 +527,7 @@ public class ScriptOperations { } else if (totalBytes > 0) { // Determinate progress in scaled units for huge files. // zh-CN: 对于超大文件使用缩放单位的确定性进度. - builder.progress(false, PROGRESS_MAX_LARGE, true); + builder.progress(false, DialogUtils.PROGRESS_MAX_LARGE, true); } else { builder.progress(true, 0); } @@ -705,13 +680,13 @@ public class ScriptOperations { @NonNull File src, @NonNull File dst, long totalBytes, - @NonNull ProgressCallback callback, + @NonNull ProgressCallback progressCallback, @NonNull FileProgressCallback fileCallback, @NonNull OperationController controller ) throws IOException { controller.throwIfCancelled(); long[] copied = new long[]{0L}; - copyFileOrDirWithProgressInternal(src, dst, totalBytes, copied, callback, fileCallback, controller); + copyFileOrDirWithProgressInternal(src, dst, totalBytes, copied, progressCallback, fileCallback, controller); } private void copyFileOrDirWithProgressInternal( @@ -719,13 +694,13 @@ public class ScriptOperations { @NonNull File dst, long totalBytes, @NonNull long[] copied, - @NonNull ProgressCallback callback, - @NonNull FileProgressCallback pathCallback, + @NonNull ProgressCallback progressCallback, + @NonNull FileProgressCallback fileCallback, @NonNull OperationController controller ) throws IOException { controller.throwIfCancelled(); - pathCallback.onFile(dst); + fileCallback.onFile(dst); if (src.isDirectory()) { if (!dst.exists() && !dst.mkdirs()) { @@ -736,12 +711,12 @@ public class ScriptOperations { return; } for (File child : children) { - copyFileOrDirWithProgressInternal(child, new File(dst, child.getName()), totalBytes, copied, callback, pathCallback, controller); + copyFileOrDirWithProgressInternal(child, new File(dst, child.getName()), totalBytes, copied, progressCallback, fileCallback, controller); } return; } - copyFileWithProgressAtomic(src, dst, totalBytes, copied, callback, pathCallback, controller); + copyFileWithProgressAtomic(src, dst, totalBytes, copied, progressCallback, fileCallback, controller); } // Copy file with an atomic "partial file then rename" strategy, supports abort cleanup. @@ -751,8 +726,8 @@ public class ScriptOperations { @NonNull File dst, long totalBytes, @NonNull long[] copied, - @NonNull ProgressCallback callback, - @NonNull FileProgressCallback pathCallback, + @NonNull ProgressCallback progressCallback, + @NonNull FileProgressCallback fileCallback, @NonNull OperationController controller ) throws IOException { controller.throwIfCancelled(); @@ -777,8 +752,8 @@ public class ScriptOperations { fos.write(buffer, 0, read); copied[0] += read; - pathCallback.onFile(dst); - callback.onProgress(copied[0], totalBytes); + fileCallback.onFile(dst); + progressCallback.onProgress(copied[0], totalBytes); } fos.getFD().sync(); } @@ -821,7 +796,7 @@ public class ScriptOperations { @NonNull File src, @NonNull File dst, long totalBytes, - @NonNull ProgressCallback callback, + @NonNull ProgressCallback progressCallback, @NonNull FileProgressCallback fileCallback, @NonNull OperationController controller ) throws IOException { @@ -833,7 +808,7 @@ public class ScriptOperations { return; } - copyFileOrDirWithProgress(src, dst, totalBytes, callback, fileCallback, controller); + copyFileOrDirWithProgress(src, dst, totalBytes, progressCallback, fileCallback, controller); controller.throwIfCancelled(); @@ -980,7 +955,7 @@ public class ScriptOperations { .negativeText(R.string.dialog_button_cancel) .positiveText(R.string.dialog_button_confirm) .positiveColorRes(R.color.dialog_button_caution); - MaterialDialogUtils.choiceWidgetThemeColor(builder); + DialogUtils.choiceWidgetThemeColor(builder); return builder.build(); }); } @@ -1007,30 +982,22 @@ public class ScriptOperations { private void moveToTrashWithProgress(final ScriptFile scriptFile) { boolean isDir = scriptFile.isDirectory(); - int titleRes = isDir ? R.string.text_delete_folder : R.string.text_delete_file; + int titleRes = isDir ? R.string.text_move_folder_to_trash : R.string.text_move_file_to_trash; Observable.fromCallable(() -> { OperationController controller = new OperationController(); ProgressDialogSession session = new ProgressDialogSession(controller); try { - // Indeterminate progress is acceptable for trashing. - // zh-CN: 移入回收站使用不确定进度条即可. MaterialDialog.Builder builder = new MaterialDialog.Builder(mContext) .title(titleRes) - .content(R.string.ellipsis_six) + .content(R.string.text_moving_to_trash) .negativeText(R.string.dialog_button_abort) .negativeColorRes(R.color.dialog_button_caution) .onNegative((dialog, which) -> { controller.cancel(); - try { - dialog.setContent(mContext.getString(R.string.text_aborting)); - } catch (Throwable ignored) { - /* Ignored. */ - } }) - .progress(true, 0) - .progressIndeterminateStyle(true) + .progress(false, DialogUtils.PROGRESS_MAX_LARGE, true) .cancelable(false) .canceledOnTouchOutside(false); @@ -1041,7 +1008,20 @@ public class ScriptOperations { // Move into trash (blob + db) then notify explorer. // zh-CN: 移入回收站 (blob + db), 然后通知资源管理器. new TrashRepository(mContext.getApplicationContext()) - .moveToTrash(scriptFile.getPath()); + .moveToTrashWithProgress( + scriptFile.getPath(), + (processed, total) -> { + controller.throwIfCancelled(); + if (total > 0) { + session.setProgressThrottled(processed, total); + } + }, + (currentFile) -> { + controller.throwIfCancelled(); + session.setContentThrottled(toContentList(scriptFile, currentFile)); + }, + controller + ); return 1; } catch (OperationAbortedException aborted) { @@ -1056,6 +1036,13 @@ public class ScriptOperations { switch (result) { case 1 -> { showMessage(R.string.text_moved_to_trash); + + // Refresh explorer reliably for both file and directory. + // zh-CN: 对文件与目录均使用更可靠的刷新方式. + notifyChildrenChangedAtItsParent(scriptFile); + + // Keep existing item removed notification as best-effort. + // zh-CN: 保留原有的 itemRemoved 通知, 作为尽力而为的补充. notifyFileRemoved(ProjectConfig.isProject(scriptFile), scriptFile.isDirectory(), scriptFile); } case -1 -> showMessage(R.string.text_operation_aborted); @@ -1064,6 +1051,17 @@ public class ScriptOperations { }, e -> { e.printStackTrace(); showMessage(R.string.text_failed_to_delete); + + String msg = e.getClass().getSimpleName(); + if (e.getMessage() != null && !e.getMessage().isBlank()) { + msg = msg + ": " + e.getMessage(); + } + DialogUtils.buildAndShowAdaptive(new MaterialDialog.Builder(mContext) + .title(R.string.text_move_to_trash_failed) + .content(msg) + .positiveText(R.string.dialog_button_dismiss) + .positiveColorRes(R.color.dialog_button_default) + .cancelable(false)::build); }); } @@ -1077,23 +1075,16 @@ public class ScriptOperations { OperationController controller = new OperationController(); ProgressDialogSession session = new ProgressDialogSession(controller); - final long[] lastContentUpdateTime = new long[]{0L}; - try { // For delete, indeterminate progress is acceptable and consistent. // zh-CN: 删除操作使用不确定进度条即可, 行为更稳定. MaterialDialog.Builder builder = new MaterialDialog.Builder(mContext) .title(titleRes) - .content(R.string.ellipsis_six) + .content(R.string.text_deleting) .negativeText(R.string.dialog_button_abort) .negativeColorRes(R.color.dialog_button_caution) .onNegative((dialog, which) -> { controller.cancel(); - try { - dialog.setContent(mContext.getString(R.string.text_aborting)); - } catch (Throwable ignored) { - /* Ignored. */ - } }) .progress(true, 0) .progressIndeterminateStyle(true) @@ -1104,7 +1095,7 @@ public class ScriptOperations { boolean deleted = deleteRecursivelyWithProgress( scriptFile, - currentFile -> session.setContentThrottled(toContentList(scriptFile, currentFile), lastContentUpdateTime), + currentFile -> session.setContentThrottled(toContentList(scriptFile, currentFile)), controller ); @@ -1122,6 +1113,20 @@ public class ScriptOperations { case 1 -> { showMessage(R.string.text_already_deleted); notifyFileRemoved(isProject, isDir, scriptFile); + + if (HistoryPrefs.deletePermanentlyAlsoClearHistory()) { + var scriptFilePath = scriptFile.getPath(); + if (scriptFilePath.startsWith(INTERNAL_STORAGE_ROOT)) { + Schedulers.io().scheduleDirect(() -> { + try { + new HistoryRepository(mContext.getApplicationContext()) + .clearHistoryForPath(scriptFilePath); + } catch (Throwable ignored) { + /* Ignored. */ + } + }); + } + } } case 0 -> showMessage(R.string.text_failed_to_delete); case -1 -> showMessage(R.string.text_operation_aborted); @@ -1131,7 +1136,7 @@ public class ScriptOperations { public void setAsWorkingDirNow(final ScriptFile scriptFile) { WorkingDirectoryUtils.setPath(scriptFile.getPath()); - WorkingDirectoryUtils.addIntoHistories(scriptFile.getPath()); + WorkingDirectoryUtils.addIntoHistory(scriptFile.getPath()); showMessage(R.string.text_done); } @@ -1288,190 +1293,11 @@ public class ScriptOperations { } } - private static final class ProgressDialogSession { - - private final OperationController controller; - private final AtomicReference dialogRef = new AtomicReference<>(); - - // Whether the session has been closed (operation finished/aborted), used to prevent late dialog showing. - // zh-CN: 会话是否已关闭 (操作已完成/已中止), 用于防止延迟任务在结束后仍弹出对话框. - private final AtomicBoolean closed = new AtomicBoolean(false); - - private final AtomicBoolean showScheduled = new AtomicBoolean(false); - - private volatile boolean shown = false; - - private long shownAtUptimeMs = 0L; - - ProgressDialogSession(OperationController controller) { - this.controller = controller; - } - - void scheduleShow(MaterialDialog.Builder builder) { - if (!showScheduled.compareAndSet(false, true)) { - return; - } - - GlobalAppContext.postDelayed(() -> { - // Skip showing if operation already finished/aborted. - // zh-CN: 若操作已结束/已中止, 则不再显示对话框. - if (closed.get() || controller.cancelled.get()) { - return; - } - - MaterialDialog dialog = DialogUtils.buildAndShowAdaptive(builder::build); - dialog.setProgressNumberFormat("..."); - DialogUtils.applyProgressThemeColorTintLists(dialog); - - // If closed becomes true right after building, dismiss immediately to avoid dangling dialog. - // zh-CN: 若 build 后立刻变为 closed, 则立即关闭, 避免对话框悬挂. - if (closed.get()) { - try { - if (dialog.isShowing()) { - dialog.dismiss(); - } - } catch (Throwable ignored) { - /* Ignored. */ - } - return; - } - - dialogRef.set(dialog); - shown = true; - shownAtUptimeMs = SystemClock.uptimeMillis(); - }, PROGRESS_DIALOG_SHOW_DELAY_MS); - } - - void setContentThrottled(List contentList, long[] lastUpdateTime) { - long now = SystemClock.uptimeMillis(); - - if (now - lastUpdateTime[0] < PROGRESS_UPDATE_MIN_INTERVAL_MS) { - return; - } - - lastUpdateTime[0] = now; - - MaterialDialog dialog = dialogRef.get(); - if (dialog == null) { - return; - } - GlobalAppContext.post(() -> { - try { - TextView contentView = dialog.getContentView(); - if (contentView != null) { - ViewUtils.setLinesEllipsizedIndividually(contentView, contentList, 1.5f); - } - } catch (Throwable ignored) { - /* Ignored. */ - } - }); - } - - void setProgressThrottled(long progress, long totalBytes, long[] lastUpdateTime, long[] lastProgress) { - long now = SystemClock.uptimeMillis(); - - if (progress == lastProgress[0]) { - return; - } - if (now - lastUpdateTime[0] < PROGRESS_UPDATE_MIN_INTERVAL_MS) { - return; - } - - lastUpdateTime[0] = now; - lastProgress[0] = progress; - - MaterialDialog dialog = dialogRef.get(); - if (dialog == null) { - return; - } - GlobalAppContext.post(() -> { - try { - DialogUtils.setProgressNumberFormatByBytes(dialog, progress, totalBytes, true); - - int v; - int max; - - if (totalBytes > 0 && totalBytes <= Integer.MAX_VALUE) { - max = (int) totalBytes; - v = (int) Math.max(0L, Math.min(progress, max)); - } else if (totalBytes > 0) { - max = PROGRESS_MAX_LARGE; - double ratio = (double) progress / (double) totalBytes; - v = (int) Math.round(ratio * (double) max); - v = Math.max(0, Math.min(max, v)); - } else { - // No total bytes, keep indeterminate behavior. - // zh-CN: 无总字节数, 保持不确定进度行为. - return; - } - - dialog.setMaxProgress(max); - dialog.setProgress(v); - } catch (Throwable ignored) { - /* Ignored. */ - } - }); - } - - void dismissSafely() { - // Mark closed first so delayed show won't create a dialog after completion. - // zh-CN: 先标记 closed, 防止延迟 show 在完成后创建对话框. - closed.set(true); - - MaterialDialog dialog = dialogRef.get(); - if (dialog == null) { - return; - } - - long now = SystemClock.uptimeMillis(); - long delay = 0L; - - if (shown) { - long shownDuration = now - shownAtUptimeMs; - if (shownDuration < PROGRESS_DIALOG_MIN_SHOW_MS) { - delay = PROGRESS_DIALOG_MIN_SHOW_MS - shownDuration; - } - } - - GlobalAppContext.postDelayed(() -> { - try { - if (dialog.isShowing()) { - dialog.dismiss(); - } - } catch (Throwable ignored) { - /* Ignored. */ - } - }, delay); - } - } - - // Controller for cancellable operations (copy/move/delete). - // zh-CN: 用于可取消操作 (复制/移动/删除) 的控制器. - private static final class OperationController { - final AtomicBoolean cancelled = new AtomicBoolean(false); - - void cancel() { - cancelled.set(true); - } - - void throwIfCancelled() { - if (cancelled.get()) { - throw new OperationAbortedException(); - } - } - } - - private static final class OperationAbortedException extends RuntimeException { - OperationAbortedException() { - super("Operation aborted"); - } - } - - private interface FileProgressCallback { + public interface FileProgressCallback { void onFile(@NonNull File currentFile); } - private interface ProgressCallback { - void onProgress(long copiedBytes, long totalBytes); + public interface ProgressCallback { + void onProgress(long processed, long total); } } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/ui/error/ErrorDialogActivity.java b/app/src/main/java/org/autojs/autojs/ui/error/ErrorDialogActivity.java index 5f5693fe..e9251aae 100644 --- a/app/src/main/java/org/autojs/autojs/ui/error/ErrorDialogActivity.java +++ b/app/src/main/java/org/autojs/autojs/ui/error/ErrorDialogActivity.java @@ -4,7 +4,7 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; import com.afollestad.materialdialogs.MaterialDialog; -import org.autojs.autojs.util.MaterialDialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.ui.BaseActivity; import org.autojs.autojs.util.ClipboardUtils; import org.autojs.autojs.util.IntentUtils; @@ -65,7 +65,7 @@ public class ErrorDialogActivity extends BaseActivity { var materialDialog = builder.show(); - MaterialDialogUtils.makeTextCopyable(materialDialog, materialDialog.getContentView()); + DialogUtils.makeTextCopyable(materialDialog, materialDialog.getContentView()); } /** diff --git a/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt b/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt index 36ff75de..e18ac63c 100644 --- a/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt +++ b/app/src/main/java/org/autojs/autojs/ui/explorer/ExplorerView.kt @@ -19,11 +19,13 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.afollestad.materialdialogs.MaterialDialog import io.reactivex.Completable import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import org.autojs.autojs.util.DialogUtils import org.autojs.autojs.core.pref.Pref.getInt import org.autojs.autojs.core.pref.Pref.getLinkedList import org.autojs.autojs.core.pref.Pref.getStringOrNull @@ -62,6 +64,7 @@ import org.autojs.autojs.ui.viewmodel.ExplorerItemManager.Companion.SORT_TYPE_SI import org.autojs.autojs.ui.viewmodel.ExplorerItemManager.Companion.SORT_TYPE_TYPE import org.autojs.autojs.ui.widget.BindableViewHolder import org.autojs.autojs.ui.widget.FirstCharView +import org.autojs.autojs.util.ClipboardUtils import org.autojs.autojs.util.ColorUtils import org.autojs.autojs.util.EnvironmentUtils.externalStoragePath import org.autojs.autojs.util.FileUtils @@ -130,7 +133,7 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef private var mOnItemOperateListener: OnItemOperateListener? = null - private val mPageStateHistories = Stack() + private val mPageStateHistory = Stack() private var mDirectorySpanSize = 2 // Keep only one refresh chain running to avoid concurrent updates. @@ -248,7 +251,7 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef override fun onDetachedFromWindow() { super.onDetachedFromWindow() mExplorer?.unregisterChangeListener(this) - mPageStateHistories.clear() + mPageStateHistory.clear() binding = null // Cancel pending refresh to avoid late UI updates and concurrent iterations. @@ -269,17 +272,20 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef R.id.action_move_to -> { ScriptOperations(context, this@ExplorerView, currentPage) .move(selectedItem!!.toScriptFile()) - .subscribe(Observers.emptyObserver()) + .ignoreElements() + .subscribe({}, { showErrorDialog(it, R.string.error_failed_to_move) }) } R.id.action_copy_to -> { ScriptOperations(context, this@ExplorerView, currentPage) .copy(selectedItem!!.toScriptFile()) - .subscribe(Observers.emptyObserver()) + .ignoreElements() + .subscribe({}, { showErrorDialog(it, R.string.error_failed_to_copy) }) } R.id.action_rename -> { ScriptOperations(context, this@ExplorerView, currentPage) .rename(selectedItem as ExplorerFileItem?) - .subscribe(Observers.emptyObserver()) + .ignoreElements() + .subscribe({}, { showErrorDialog(it, R.string.error_failed_to_rename) }) } R.id.action_delete -> { ScriptOperations(context, this@ExplorerView, currentPage) @@ -290,7 +296,7 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef // Show version history for this file and restore to disk when chosen. // zh-CN: 显示该文件的版本历史, 选择后写回磁盘恢复. - VersionHistoryController(context).showForFilePath(selected.path) + VersionHistoryController(context).showForFilePath(selected.path, selected.path) notifyItemOperated() mRequestHostDialogHide?.run() @@ -348,6 +354,30 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef return true } + private fun showErrorDialog(t: Throwable, titleRes: Int) { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(context) + .title(titleRes) + .content(t.message ?: context.getString(R.string.error_unknown_exception)) + .apply { + t.message?.let { tMessage -> + neutralText(R.string.dialog_button_copy) + neutralColorRes(R.color.dialog_button_hint) + onNeutral { d, _ -> + ClipboardUtils.setClip(context, tMessage) + showSnack(d.view, R.string.text_already_copied_to_clip, false) + } + } + } + .positiveText(R.string.dialog_button_dismiss) + .positiveColorRes(R.color.dialog_button_default) + .onPositive { d, _ -> d.dismiss() } + .cancelable(false) + .autoDismiss(false) + .build() + } + } + protected open fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup?, viewType: Int): BindableViewHolder { return when (viewType) { VIEW_TYPE_ITEM -> ExplorerItemViewHolder(inflater.inflate(R.layout.explorer_file, parent, false)) @@ -447,11 +477,11 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef } fun canGoBack(): Boolean { - return !mPageStateHistories.empty() + return !mPageStateHistory.empty() } fun goBack() { - setCurrentPageState(mPageStateHistories.pop()) + setCurrentPageState(mPageStateHistory.pop()) refreshCurrentPage() } @@ -508,19 +538,19 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef val currentPath = currentPage.path putString(getPrefKey("explorer_current"), currentPath) - val rootPath = if (mPageStateHistories.isEmpty()) path else mPageStateHistories.firstElement().page!!.path + val rootPath = if (mPageStateHistory.isEmpty()) path else mPageStateHistory.firstElement().page!!.path putString(getPrefKey("explorer_root"), rootPath) - val histories = LinkedList() - for (explorerPageState in mPageStateHistories) { - histories.add( + val historyItems = LinkedList() + for (explorerPageState in mPageStateHistory) { + historyItems.add( (explorerPageState.page!!.path + "," + explorerPageState.scrollY + "," + explorerPageState.dirsCollapsed + "," + explorerPageState.filesCollapsed) ) } - putLinkedList(getPrefKey("explorer_histories"), histories) + putLinkedList(getPrefKey("explorer_history"), historyItems) } // TODO by SuperMonster003 on Apr 1, 2023. @@ -535,11 +565,11 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef setDefaultExplorer() } - val storedHistories = getLinkedList(getPrefKey("explorer_histories")) - for (dataString in storedHistories) { + val storedHistory = getLinkedList(getPrefKey("explorer_history")) + for (dataString in storedHistory) { val split = dataString.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() val (pagePath, scrollY, dirsCollapsed, filesCollapsed) = split - mPageStateHistories.push(ExplorerPageState(ExplorerDirPage.createRoot(pagePath)).also { pageState -> + mPageStateHistory.push(ExplorerPageState(ExplorerDirPage.createRoot(pagePath)).also { pageState -> pageState.scrollY = scrollY.toInt() pageState.dirsCollapsed = dirsCollapsed.toBoolean() pageState.filesCollapsed = filesCollapsed.toBoolean() @@ -564,7 +594,7 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef } fun saveCurrentPageIntoHistory() { - mPageStateHistories.push(currentPageState) + mPageStateHistory.push(currentPageState) } fun setDirectorySpanSize(directorySpanSize: Int) { @@ -1013,6 +1043,9 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef if (!mExplorerItem.canDelete()) { menu.removeItem(R.id.action_delete) } + if (!mExplorerItem.isTextEditable) { + menu.removeItem(R.id.action_version_history) + } if (!mExplorerItem.canBuildApk()) { menu.removeItem(R.id.action_build_apk) } @@ -1101,15 +1134,18 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef R.id.action_copy_to -> ScriptOperations(context, explorerView, currentPage) .copy(selectedItem.toScriptFile()) - .subscribe(Observers.emptyObserver()) + .ignoreElements() + .subscribe({}, { showErrorDialog(it, R.string.error_failed_to_copy) }) R.id.action_move_to -> ScriptOperations(context, explorerView, currentPage) .move(selectedItem.toScriptFile()) - .subscribe(Observers.emptyObserver()) + .ignoreElements() + .subscribe({}, { showErrorDialog(it, R.string.error_failed_to_move) }) R.id.action_rename -> ScriptOperations(context, explorerView, currentPage) .rename(selectedItem as? ExplorerFileItem) - .subscribe(Observers.emptyObserver()) + .ignoreElements() + .subscribe({}, { showErrorDialog(it, R.string.error_failed_to_rename) }) R.id.action_delete -> ScriptOperations(context, explorerView, currentPage) .delete(selectedItem.toScriptFile()) @@ -1290,7 +1326,7 @@ open class ExplorerView : ThemeColorSwipeRefreshLayout, SwipeRefreshLayout.OnRef remove(getPrefKey("page_state")) remove(getPrefKey("explorer_current")) remove(getPrefKey("explorer_root")) - remove(getPrefKey("explorer_histories")) + remove(getPrefKey("explorer_history")) } fun getPrefKey(key: String): String = ExplorerView::class.java.simpleName + '.' + key diff --git a/app/src/main/java/org/autojs/autojs/ui/filechooser/FileChooserDialogBuilder.java b/app/src/main/java/org/autojs/autojs/ui/filechooser/FileChooserDialogBuilder.java index a5589807..5689f907 100644 --- a/app/src/main/java/org/autojs/autojs/ui/filechooser/FileChooserDialogBuilder.java +++ b/app/src/main/java/org/autojs/autojs/ui/filechooser/FileChooserDialogBuilder.java @@ -7,7 +7,7 @@ import androidx.annotation.StringRes; import com.afollestad.materialdialogs.MaterialDialog; import io.reactivex.Observable; import io.reactivex.subjects.PublishSubject; -import org.autojs.autojs.app.DialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.model.explorer.Explorer; import org.autojs.autojs.model.explorer.ExplorerDirPage; import org.autojs.autojs.model.explorer.ExplorerFileProvider; diff --git a/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java b/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java index 1c9ec9ee..b7743b5a 100644 --- a/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java +++ b/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java @@ -14,7 +14,7 @@ import com.makeramen.roundedimageview.RoundedImageView; import org.autojs.autojs.AutoJs; import org.autojs.autojs.app.AppLevelThemeDialogBuilder; import org.autojs.autojs.app.CircularMenuOperationDialogBuilder; -import org.autojs.autojs.app.DialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.app.tool.PointerLocationTool; import org.autojs.autojs.core.accessibility.AccessibilityTool; import org.autojs.autojs.core.accessibility.Capture; diff --git a/app/src/main/java/org/autojs/autojs/ui/floating/CodeGenerateDialog.java b/app/src/main/java/org/autojs/autojs/ui/floating/CodeGenerateDialog.java index 2be0b7a6..848b1b03 100644 --- a/app/src/main/java/org/autojs/autojs/ui/floating/CodeGenerateDialog.java +++ b/app/src/main/java/org/autojs/autojs/ui/floating/CodeGenerateDialog.java @@ -20,7 +20,7 @@ import com.bignerdranch.expandablerecyclerview.ParentViewHolder; import com.bignerdranch.expandablerecyclerview.model.Parent; import org.autojs.autojs.app.AppLevelThemeDialogBuilder; -import org.autojs.autojs.app.DialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.codegeneration.CodeGenerator; import org.autojs.autojs.core.accessibility.NodeInfo; import org.autojs.autojs.theme.util.ListBuilder; diff --git a/app/src/main/java/org/autojs/autojs/ui/floating/LayoutFloatyWindow.kt b/app/src/main/java/org/autojs/autojs/ui/floating/LayoutFloatyWindow.kt index b36cab0d..eea258e5 100644 --- a/app/src/main/java/org/autojs/autojs/ui/floating/LayoutFloatyWindow.kt +++ b/app/src/main/java/org/autojs/autojs/ui/floating/LayoutFloatyWindow.kt @@ -5,7 +5,7 @@ import android.view.ContextThemeWrapper import android.view.View import android.view.ViewGroup import org.autojs.autojs.app.AppLevelThemeDialogBuilder -import org.autojs.autojs.app.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.showAdaptive import org.autojs.autojs.core.accessibility.Capture import org.autojs.autojs.core.accessibility.NodeInfo import org.autojs.autojs.core.accessibility.WindowInfo diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt index 5f1fc23d..d965287a 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerFragment.kt @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable import org.autojs.autojs.AutoJs import org.autojs.autojs.app.tool.FloatingButtonTool import org.autojs.autojs.app.tool.JsonSocketClientTool @@ -21,6 +22,7 @@ import org.autojs.autojs.app.tool.PointerLocationTool import org.autojs.autojs.core.accessibility.AccessibilityTool import org.autojs.autojs.core.plugin.center.PluginCenterActivity import org.autojs.autojs.core.pref.Pref +import org.autojs.autojs.core.pref.PrefRx import org.autojs.autojs.external.foreground.AppForegroundService import org.autojs.autojs.permission.AllFilesAccessPermission import org.autojs.autojs.permission.DisplayOverOtherAppsPermission @@ -36,10 +38,13 @@ import org.autojs.autojs.permission.XiaomiBackgroundPopupPermission import org.autojs.autojs.pluginclient.DevPluginService import org.autojs.autojs.pluginclient.JsonSocketClient import org.autojs.autojs.pluginclient.JsonSocketServer +import org.autojs.autojs.pio.PFiles import org.autojs.autojs.runtime.api.WrappedShizuku import org.autojs.autojs.service.AccessibilityService import org.autojs.autojs.service.ForegroundService import org.autojs.autojs.service.NotificationService +import org.autojs.autojs.storage.history.HistoryDatabase +import org.autojs.autojs.storage.history.HistoryPrefs import org.autojs.autojs.theme.ThemeChangeNotifier import org.autojs.autojs.theme.app.ColorSelectBaseActivity import org.autojs.autojs.ui.floating.CircularMenu @@ -66,18 +71,26 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import rikka.shizuku.Shizuku import java.lang.ref.WeakReference +import java.util.Locale import kotlin.math.roundToInt import org.autojs.autojs.util.App as UtilApp /** * Created by Stardust on Jan 30, 2017. - * Modified by SuperMonster003 as of Jan 9, 2026. * Transformed by SuperMonster003 on Sep 19, 2022. + * Modified by SuperMonster003 as of Jan 9, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 5, 2026. */ open class DrawerFragment : Fragment() { private val binding by viewBinding(FragmentDrawerBinding::bind) + /** + * Rx disposables for drawer subtitle observing. + * zh-CN: 抽屉子标题监听的 Rx 订阅管理器. + */ + private val drawerStatsDisposables: CompositeDisposable = CompositeDisposable() + private var privateDrawerMenu: RecyclerView? = null private var privateContext: WeakReference? = null private var privateActivity: WeakReference? = null @@ -632,8 +645,88 @@ open class DrawerFragment : Fragment() { initMenuItems() initMenuItemStates() setupListeners() + + // Start observing stats for drawer subtitles. + // zh-CN: 开始监听抽屉子标题所需的统计信息. + observeTrashAndHistoryDrawerSubtitles() } + private fun observeTrashAndHistoryDrawerSubtitles() { + val appCtx = mContext.applicationContext + val dao = HistoryDatabase.getInstance(appCtx).historyDao() + + // Trash: observe DB stats + observe limit preference. + // zh-CN: 回收站: 监听 DB 统计 + 监听容量上限偏好. + val trashLimitFlow = PrefRx.observeLong( + keyRes = R.string.key_trash_max_total_bytes, + defaultValue = HistoryPrefs.DEFAULT_TRASH_MAX_TOTAL_BYTES, + ) + + val trashDisposable = Observable + .combineLatest( + dao.observeTrashStats().toObservable(), + trashLimitFlow.toObservable(), + ) { stats, limitBytes -> + val count = stats.count.coerceAtLeast(0L) + val totalBytes = (stats.totalBytes ?: 0L).coerceAtLeast(0L) + val limit = limitBytes.coerceAtLeast(0L) + + val sizeText = PFiles.formatSizeWithUnit(totalBytes) + + val percentText = if (limit > 0L) { + val ratio = (totalBytes.toDouble() / limit.toDouble()).coerceAtLeast(0.0) + String.format(Locale.getDefault(), "%.1f%%", ratio * 100.0) + } else { + String.format(Locale.getDefault(), "%.1f%%", 0.0) + } + + "$count | $sizeText | $percentText" + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { subtitle -> + mTrashItem.subtitle = subtitle + (mDrawerMenu.adapter as? DrawerMenuAdapter)?.notifyItemChanged(mTrashItem) + } + + drawerStatsDisposables.add(trashDisposable) + + // Version history: observe DB stats + observe limit preference. + // zh-CN: 版本历史: 监听 DB 统计 + 监听容量上限偏好. + val historyLimitFlow = PrefRx.observeLong( + keyRes = R.string.key_history_max_total_bytes, + defaultValue = HistoryPrefs.DEFAULT_HISTORY_MAX_TOTAL_BYTES, + ) + + val historyDisposable = Observable + .combineLatest( + dao.observeVersionHistoryStats().toObservable(), + historyLimitFlow.toObservable(), + ) { stats, limitBytes -> + val count = stats.fileCount.coerceAtLeast(0L) + val totalBytes = (stats.totalBytes ?: 0L).coerceAtLeast(0L) + val limit = limitBytes.coerceAtLeast(0L) + + val sizeText = PFiles.formatSizeWithUnit(totalBytes) + + val percentText = if (limit > 0L) { + val ratio = (totalBytes.toDouble() / limit.toDouble()).coerceAtLeast(0.0) + String.format(Locale.getDefault(), "%.1f%%", ratio * 100.0) + } else { + String.format(Locale.getDefault(), "%.1f%%", 0.0) + } + + "$count | $sizeText | $percentText" + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { subtitle -> + mVersionHistoryItem.subtitle = subtitle + (mDrawerMenu.adapter as? DrawerMenuAdapter)?.notifyItemChanged(mVersionHistoryItem) + } + + drawerStatsDisposables.add(historyDisposable) + } + + private fun configureDrawerWidth() { val screenWidthDp = resources.configuration.screenWidthDp val targetWidthPx = 288f.coerceIn( @@ -659,7 +752,13 @@ open class DrawerFragment : Fragment() { super.onDestroy() mClientModeItem.dispose() mServerModeItem.dispose() + // mActivity.unregisterReceiver(mReceiver) + + // Dispose drawer subtitle observers. + // zh-CN: 释放抽屉子标题监听订阅. + drawerStatsDisposables.clear() + EventBus.getDefault().unregister(this) } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt index bac131aa..e8e6e289 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/drawer/DrawerMenuToggleableItem.kt @@ -8,7 +8,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import org.autojs.autojs.core.pref.Pref import org.autojs.autojs.ui.common.NotAskAgainDialog -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt index 40626336..ec3975eb 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/ApkInfoDialogManager.kt @@ -25,10 +25,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select import kotlinx.coroutines.withContext import net.dongliu.apk.parser.ApkFile -import org.autojs.autojs.app.DialogUtils.showAdaptive -import org.autojs.autojs.util.MaterialDialogUtils.makeSettingsLaunchable -import org.autojs.autojs.util.MaterialDialogUtils.makeTextCopyable -import org.autojs.autojs.util.MaterialDialogUtils.setCopyableTextIfAbsent +import org.autojs.autojs.util.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.makeSettingsLaunchable +import org.autojs.autojs.util.DialogUtils.makeTextCopyable +import org.autojs.autojs.util.DialogUtils.setCopyableTextIfAbsent import org.autojs.autojs.external.fileprovider.AppFileProvider import org.autojs.autojs.pio.PFiles import org.autojs.autojs.runtime.api.AppUtils diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/EditableFileInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/EditableFileInfoDialogManager.kt index 165c4ce8..63381484 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/EditableFileInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/EditableFileInfoDialogManager.kt @@ -13,9 +13,9 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.autojs.autojs.app.DialogUtils.showAdaptive -import org.autojs.autojs.util.MaterialDialogUtils.makeTextCopyable -import org.autojs.autojs.util.MaterialDialogUtils.setCopyableTextIfAbsent +import org.autojs.autojs.util.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.makeTextCopyable +import org.autojs.autojs.util.DialogUtils.setCopyableTextIfAbsent import org.autojs.autojs.external.fileprovider.AppFileProvider import org.autojs.autojs.pio.PFiles import org.autojs.autojs.runtime.api.Mime @@ -38,6 +38,8 @@ object EditableFileInfoDialogManager { MaterialDialog.Builder(context) .title(R.string.text_failed) .content(R.string.file_not_exist_or_readable) + .positiveText(R.string.dialog_button_dismiss) + .positiveColorRes(R.color.dialog_button_default) .showAdaptive() return } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/scripts/MediaInfoDialogManager.kt b/app/src/main/java/org/autojs/autojs/ui/main/scripts/MediaInfoDialogManager.kt index fd98e226..74badd3e 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/scripts/MediaInfoDialogManager.kt +++ b/app/src/main/java/org/autojs/autojs/ui/main/scripts/MediaInfoDialogManager.kt @@ -17,9 +17,9 @@ import kotlinx.coroutines.async import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.autojs.autojs.app.DialogUtils.showAdaptive -import org.autojs.autojs.util.MaterialDialogUtils.makeTextCopyable -import org.autojs.autojs.util.MaterialDialogUtils.setCopyableText +import org.autojs.autojs.util.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.makeTextCopyable +import org.autojs.autojs.util.DialogUtils.setCopyableText import org.autojs.autojs.model.explorer.ExplorerItem import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R diff --git a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java index db07fea9..6c1b78c2 100644 --- a/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java +++ b/app/src/main/java/org/autojs/autojs/ui/project/BuildActivity.java @@ -35,7 +35,7 @@ import net.dongliu.apk.parser.bean.ApkMeta; import org.autojs.autojs.apkbuilder.ApkBuilder; import org.autojs.autojs.apkbuilder.keystore.KeyStore; import org.autojs.autojs.core.pref.Language; -import org.autojs.autojs.util.MaterialDialogUtils; +import org.autojs.autojs.util.DialogUtils; import org.autojs.autojs.external.fileprovider.AppFileProvider; import org.autojs.autojs.model.explorer.Explorers; import org.autojs.autojs.model.script.ScriptFile; @@ -422,7 +422,7 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa getString(R.string.uri_autojs6_download_link)); builder.positiveText(R.string.dialog_button_dismiss); builder.positiveColorRes(R.color.dialog_button_hint); - MaterialDialogUtils.widgetThemeColor(builder); + DialogUtils.widgetThemeColor(builder); MaterialDialog dialog = builder.show(); if (dialog != null) { TextView contentView = dialog.getContentView(); diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt b/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt index 0b999593..0f20728f 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/AboutActivity.kt @@ -11,7 +11,7 @@ import com.afollestad.materialdialogs.MaterialDialog import com.jaredrummler.android.widget.AnimatedSvgView import de.psdev.licensesdialog.LicenseResolver import de.psdev.licensesdialog.LicensesDialog -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.network.UpdateChecker import org.autojs.autojs.network.UpdateChecker.PromptMode import org.autojs.autojs.ui.BaseActivity @@ -66,7 +66,7 @@ open class AboutActivity : BaseActivity() { functionsButtonsBinding.aboutFunctionsButtonLicenses.setOnClickListener { showLicensesDialog() } functionsButtonsBinding.aboutFunctionsButtonUpdate.setOnClickListener { checkForUpdates() } - functionsButtonsBinding.aboutFunctionsButtonVersionHistories.setOnClickListener { showVersionHistories() } + functionsButtonsBinding.aboutFunctionsButtonReleaseHistory.setOnClickListener { showReleaseHistory() } functionsButtonsBinding.aboutFunctionsButtonFeedback.setOnClickListener { startFeedbackActivity() } } @@ -168,8 +168,8 @@ open class AboutActivity : BaseActivity() { .build().checkNow() } - private fun showVersionHistories() { - DisplayVersionHistoriesActivity.launch(this) + private fun showReleaseHistory() { + DisplayReleaseHistoryActivity.launch(this) } private fun startFeedbackActivity() { diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/DisplayVersionHistoriesActivity.kt b/app/src/main/java/org/autojs/autojs/ui/settings/DisplayReleaseHistoryActivity.kt similarity index 88% rename from app/src/main/java/org/autojs/autojs/ui/settings/DisplayVersionHistoriesActivity.kt rename to app/src/main/java/org/autojs/autojs/ui/settings/DisplayReleaseHistoryActivity.kt index 0898fa0f..859a5290 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/DisplayVersionHistoriesActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/DisplayReleaseHistoryActivity.kt @@ -31,25 +31,25 @@ import org.autojs.autojs.core.pref.Language.Companion.getPrefLanguageOrNull import org.autojs.autojs.theme.ThemeColorHelper import org.autojs.autojs.theme.widget.ThemeColorToolbar import org.autojs.autojs.ui.BaseActivity -import org.autojs.autojs.ui.settings.VersionHistoryRepository.Companion.Category -import org.autojs.autojs.ui.settings.VersionHistoryRepository.Companion.DEFAULT_FILTER -import org.autojs.autojs.ui.settings.VersionHistoryRepository.Companion.DEFAULT_VERSION_NAME +import org.autojs.autojs.ui.settings.ReleaseHistoryRepository.Companion.Category +import org.autojs.autojs.ui.settings.ReleaseHistoryRepository.Companion.DEFAULT_FILTER +import org.autojs.autojs.ui.settings.ReleaseHistoryRepository.Companion.DEFAULT_VERSION_NAME import org.autojs.autojs.util.IntentUtils.startSafely -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor import org.autojs.autojs.util.ProcessLogger import org.autojs.autojs.util.ViewUtils.excludePaddingClippableViewFromBottomNavigationBar import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance import org.autojs.autojs6.R -import org.autojs.autojs6.databinding.ActivityDisplayVersionHistoriesBinding +import org.autojs.autojs6.databinding.ActivityDisplayReleaseHistoryBinding -class DisplayVersionHistoriesActivity : BaseActivity() { +class DisplayReleaseHistoryActivity : BaseActivity() { - private val repo by lazy { VersionHistoryRepository() } + private val repo by lazy { ReleaseHistoryRepository() } - private lateinit var binding: ActivityDisplayVersionHistoriesBinding + private lateinit var binding: ActivityDisplayReleaseHistoryBinding - private lateinit var mAdapter: VersionHistoryAdapter + private lateinit var mAdapter: ReleaseHistoryAdapter private lateinit var mToolbar: ThemeColorToolbar private var mAllDataHandled = false @@ -58,13 +58,13 @@ class DisplayVersionHistoriesActivity : BaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityDisplayVersionHistoriesBinding.inflate(layoutInflater).also { + val binding = ActivityDisplayReleaseHistoryBinding.inflate(layoutInflater).also { binding = it mToolbar = binding.toolbar setContentView(it.root) } - val adapter = VersionHistoryAdapter(this, Markwon.create(this)).also { + val adapter = ReleaseHistoryAdapter(this, Markwon.create(this)).also { mAdapter = it } @@ -86,8 +86,8 @@ class DisplayVersionHistoriesActivity : BaseActivity() { val urlBlob = "https://github.com/SuperMonster003/AutoJs6/blob/master/$urlSuffix" val localItems = withContext(Dispatchers.IO) { - VersionHistoryRepository.readBestLocalSample( - context = this@DisplayVersionHistoriesActivity, + ReleaseHistoryRepository.readBestLocalSample( + context = this@DisplayReleaseHistoryActivity, languageTag = languageTag, ) } @@ -103,8 +103,8 @@ class DisplayVersionHistoriesActivity : BaseActivity() { var onlineItemLatestVersion: String? = null var shouldContinueCollect = true - repo.loadVersionHistoriesFlow( - context = this@DisplayVersionHistoriesActivity, + repo.loadReleaseHistoryFlow( + context = this@DisplayReleaseHistoryActivity, languageTag = languageTag, urlRaw = urlRaw, urlBlob = urlBlob @@ -115,7 +115,7 @@ class DisplayVersionHistoriesActivity : BaseActivity() { } if (onlineItemLatestVersion == null) { onlineItemLatestVersion = onlineItem.version - if (VersionHistoryRepository.compareVersion(localItemLatestVersion, onlineItemLatestVersion) > 0) { + if (ReleaseHistoryRepository.compareVersion(localItemLatestVersion, onlineItemLatestVersion) > 0) { shouldContinueCollect = false ProcessLogger.i(getString(R.string.logger_ver_history_local_data_newer_stop_online)) return@collectLatest @@ -128,7 +128,7 @@ class DisplayVersionHistoriesActivity : BaseActivity() { mAllDataHandled = true } - setToolbarAsBack(R.string.text_version_histories) + setToolbarAsBack(R.string.text_release_history) } private suspend fun hideLoadingTextContainerIfNeeded() { @@ -144,7 +144,7 @@ class DisplayVersionHistoriesActivity : BaseActivity() { } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_display_version_histories, menu) + menuInflater.inflate(R.menu.menu_display_release_history, menu) setUpToolbarColors() return super.onCreateOptionsMenu(menu) } @@ -253,7 +253,7 @@ class DisplayVersionHistoriesActivity : BaseActivity() { @JvmStatic fun launch(context: Context) { - Intent(context, DisplayVersionHistoriesActivity::class.java) + Intent(context, DisplayReleaseHistoryActivity::class.java) .startSafely(context) } diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/LauncherIconPreference.kt b/app/src/main/java/org/autojs/autojs/ui/settings/LauncherIconPreference.kt index c88bde61..d0733f6b 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/LauncherIconPreference.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/LauncherIconPreference.kt @@ -13,7 +13,7 @@ import android.util.AttributeSet import androidx.preference.PreferenceManager import com.afollestad.materialdialogs.MaterialDialog import org.autojs.autojs.core.pref.Pref -import org.autojs.autojs.util.MaterialDialogUtils.widgetThemeColor +import org.autojs.autojs.util.DialogUtils.widgetThemeColor import org.autojs.autojs.external.shortcut.ShortcutActivity import org.autojs.autojs.theme.preference.MaterialListPreference import org.autojs.autojs.ui.common.NotAskAgainDialog diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryAdapter.kt b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryAdapter.kt similarity index 85% rename from app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryAdapter.kt rename to app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryAdapter.kt index d7530141..8d3fe761 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryAdapter.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryAdapter.kt @@ -11,33 +11,33 @@ import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import io.noties.markwon.Markwon -import org.autojs.autojs.ui.settings.VersionHistoryAdapter.VersionHistoryViewHolder -import org.autojs.autojs.ui.settings.VersionHistoryRepository.Companion.Category -import org.autojs.autojs.ui.settings.VersionHistoryRepository.Companion.DEFAULT_FILTER -import org.autojs.autojs.ui.settings.VersionHistoryRepository.Companion.compareVersion +import org.autojs.autojs.ui.settings.ReleaseHistoryAdapter.ReleaseHistoryViewHolder +import org.autojs.autojs.ui.settings.ReleaseHistoryRepository.Companion.Category +import org.autojs.autojs.ui.settings.ReleaseHistoryRepository.Companion.DEFAULT_FILTER +import org.autojs.autojs.ui.settings.ReleaseHistoryRepository.Companion.compareVersion import org.autojs.autojs.util.ProcessLogger import org.autojs.autojs6.R -import org.autojs.autojs6.databinding.ItemVersionHistoryBinding +import org.autojs.autojs6.databinding.ItemReleaseHistoryBinding @SuppressLint("NotifyDataSetChanged") -class VersionHistoryAdapter(private val context: Context, private val markwon: Markwon) : RecyclerView.Adapter() { +class ReleaseHistoryAdapter(private val context: Context, private val markwon: Markwon) : RecyclerView.Adapter() { - private val mData = mutableListOf() + private val mData = mutableListOf() private var mLayoutManager: RecyclerView.LayoutManager? = null private val mCategoryFilter: MutableSet = DEFAULT_FILTER.clone() - fun submit(list: MutableList) { + fun submit(list: MutableList) { mData.clear() mData += list notifyDataSetChanged() } - fun add(item: VersionHistoryItem) { + fun add(item: ReleaseHistoryItem) { mData += item notifyItemInserted(mData.lastIndex) } - fun addAll(list: List) { + fun addAll(list: List) { val start = mData.size mData += list notifyItemRangeInserted(start, list.size) @@ -49,7 +49,7 @@ class VersionHistoryAdapter(private val context: Context, private val markwon: M notifyDataSetChanged() } - fun addOrUpdate(item: VersionHistoryItem) { + fun addOrUpdate(item: ReleaseHistoryItem) { when (val idx = mData.indexOfFirst { compareVersion(it.version, item.version) == 0 }) { -1 -> { ProcessLogger.i("${item.version}: ${context.getString(R.string.logger_ver_history_insert_new_entries)}") @@ -60,8 +60,8 @@ class VersionHistoryAdapter(private val context: Context, private val markwon: M mLayoutManager?.scrollToPosition(0) } else -> { - fun sameDate(local: VersionHistoryItem) = local.date == item.date - fun sameLines(local: VersionHistoryItem) = local.lines.joinToString(",") { it.trim() } == item.lines.joinToString(",") { it.trim() } + fun sameDate(local: ReleaseHistoryItem) = local.date == item.date + fun sameLines(local: ReleaseHistoryItem) = local.lines.joinToString(",") { it.trim() } == item.lines.joinToString(",") { it.trim() } val local = mData[idx] if (sameDate(local) && sameLines(local)) { ProcessLogger.i("${item.version}: ${context.getString(R.string.logger_ver_history_no_processing_needed)}") @@ -88,18 +88,18 @@ class VersionHistoryAdapter(private val context: Context, private val markwon: M mLayoutManager = recyclerView.layoutManager } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VersionHistoryViewHolder { - val binding = ItemVersionHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return VersionHistoryViewHolder(parent.context, binding) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ReleaseHistoryViewHolder { + val binding = ItemReleaseHistoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ReleaseHistoryViewHolder(parent.context, binding) } override fun getItemCount() = mData.size - override fun onBindViewHolder(holder: VersionHistoryViewHolder, position: Int) { + override fun onBindViewHolder(holder: ReleaseHistoryViewHolder, position: Int) { holder.bind(mData[position], animate = false) } - override fun onBindViewHolder(holder: VersionHistoryViewHolder, position: Int, payloads: MutableList) { + override fun onBindViewHolder(holder: ReleaseHistoryViewHolder, position: Int, payloads: MutableList) { val animate = payloads.contains(PAYLOAD_EXPAND_STATE_CHANGED) holder.bind(mData[position], animate) } @@ -114,7 +114,7 @@ class VersionHistoryAdapter(private val context: Context, private val markwon: M notifyItemRangeChanged(0, itemCount, PAYLOAD_EXPAND_STATE_CHANGED) } - inner class VersionHistoryViewHolder(context: Context, binding: ItemVersionHistoryBinding) : RecyclerView.ViewHolder(binding.root) { + inner class ReleaseHistoryViewHolder(context: Context, binding: ItemReleaseHistoryBinding) : RecyclerView.ViewHolder(binding.root) { private val chevron = binding.chevron @@ -140,7 +140,7 @@ class VersionHistoryAdapter(private val context: Context, private val markwon: M private val changelogLabelImprovement = "`${context.getString(R.string.changelog_label_improvement)}`" private val changelogLabelDependency = "`${context.getString(R.string.changelog_label_dependency)}`" - fun bind(item: VersionHistoryItem, animate: Boolean) { + fun bind(item: ReleaseHistoryItem, animate: Boolean) { val rotationAngle = if (item.expanded) 180f else 0f chevron.animate().cancel() when (animate) { @@ -205,7 +205,7 @@ class VersionHistoryAdapter(private val context: Context, private val markwon: M companion object { - private val TAG = VersionHistoryAdapter::class.java.simpleName + private val TAG = ReleaseHistoryAdapter::class.java.simpleName private const val PAYLOAD_EXPAND_STATE_CHANGED = 1 diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryItem.kt b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryItem.kt similarity index 82% rename from app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryItem.kt rename to app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryItem.kt index 97035d7c..5d7e3ff4 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryItem.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryItem.kt @@ -1,6 +1,6 @@ package org.autojs.autojs.ui.settings -data class VersionHistoryItem( +data class ReleaseHistoryItem( val version: String, val date: String, val lines: List, diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryPreference.kt b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryPreference.kt similarity index 82% rename from app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryPreference.kt rename to app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryPreference.kt index 9109b652..0553cb00 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryPreference.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryPreference.kt @@ -7,7 +7,7 @@ import org.autojs.autojs.theme.preference.MaterialPreference /** * Created by SuperMonster003 on May 1, 2025. */ -class VersionHistoryPreference : MaterialPreference { +class ReleaseHistoryPreference : MaterialPreference { constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) @@ -17,6 +17,6 @@ class VersionHistoryPreference : MaterialPreference { constructor(context: Context) : super(context) - override fun onClick() = DisplayVersionHistoriesActivity.launch(context) + override fun onClick() = DisplayReleaseHistoryActivity.launch(context) } \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryRepository.kt b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryRepository.kt similarity index 95% rename from app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryRepository.kt rename to app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryRepository.kt index 7ec42ffa..04d3e257 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/VersionHistoryRepository.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/ReleaseHistoryRepository.kt @@ -1,7 +1,6 @@ package org.autojs.autojs.ui.settings import android.content.Context -import androidx.annotation.StringRes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow @@ -18,16 +17,16 @@ import java.io.File import org.autojs.autojs6.R import java.util.EnumSet -class VersionHistoryRepository { +class ReleaseHistoryRepository { private val mOkHttpClient by lazy { OkHttpClient() } - fun loadVersionHistoriesFlow( + fun loadReleaseHistoryFlow( context: Context, languageTag: String, urlRaw: String, urlBlob: String, - ): Flow = channelFlow { + ): Flow = channelFlow { runCatching { ProcessLogger.i(context.getString(R.string.logger_ver_history_start_raw_thread)) ProcessLogger.i("URL: $urlRaw") @@ -107,7 +106,7 @@ class VersionHistoryRepository { return "CHANGELOG-$this.cache.md" } - suspend fun readBestLocalSample(context: Context, languageTag: String): List { + suspend fun readBestLocalSample(context: Context, languageTag: String): List { val assetStr = runCatching { context.assets.open("doc/${languageTag.toChangelogMarkdownFile()}").bufferedReader().use { it.readText() } }.getOrNull() @@ -162,14 +161,14 @@ class VersionHistoryRepository { File(context.filesDir, languageTag.toChangelogMarkdownCacheFile()).writeText(markdown) } - private fun parseMarkdownFlow(md: String): Flow = channelFlow { + private fun parseMarkdownFlow(md: String): Flow = channelFlow { var curTitle = "" var curDate = "" val bodyLines = mutableListOf() fun flush(isClose: Boolean = false) { if (curTitle.isNotBlank()) { - trySend(VersionHistoryItem(curTitle, curDate, bodyLines.toList())) + trySend(ReleaseHistoryItem(curTitle, curDate, bodyLines.toList())) bodyLines.clear() } if (isClose) close() @@ -206,7 +205,7 @@ class VersionHistoryRepository { val lines = ul.getElementsByTag("li").map { li -> TextUtils.htmlToMarkdown(li.outerHtml()).trim() } - send(VersionHistoryItem(version, date, lines)) + send(ReleaseHistoryItem(version, date, lines)) } } } @@ -225,4 +224,4 @@ class VersionHistoryRepository { } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/autojs/autojs/ui/settings/WorkingDirectoryPreference.kt b/app/src/main/java/org/autojs/autojs/ui/settings/WorkingDirectoryPreference.kt index ddeb5cb3..81990324 100644 --- a/app/src/main/java/org/autojs/autojs/ui/settings/WorkingDirectoryPreference.kt +++ b/app/src/main/java/org/autojs/autojs/ui/settings/WorkingDirectoryPreference.kt @@ -16,8 +16,8 @@ import com.afollestad.materialdialogs.MaterialDialog import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers -import org.autojs.autojs.app.DialogUtils -import org.autojs.autojs.app.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils +import org.autojs.autojs.util.DialogUtils.showAdaptive import org.autojs.autojs.storage.file.FileObservable import org.autojs.autojs.theme.ThemeColorHelper import org.autojs.autojs.theme.preference.MaterialPreference @@ -25,7 +25,7 @@ import org.autojs.autojs.tool.SimpleObserver import org.autojs.autojs.ui.filechooser.FileChooserDialogBuilder import org.autojs.autojs.ui.main.MainActivity import org.autojs.autojs.util.EnvironmentUtils -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor import org.autojs.autojs.util.ViewUtils import org.autojs.autojs.util.WorkingDirectoryUtils import org.autojs.autojs6.R @@ -103,14 +103,14 @@ class WorkingDirectoryPreference : MaterialPreference { .neutralColorRes(R.color.dialog_button_hint) .onNeutral { _, _ -> MaterialDialog.Builder(prefContext) - .title(R.string.text_histories) - .content(R.string.text_no_histories) - .items(WorkingDirectoryUtils.histories) - .itemsCallback { dHistories, _, _, text -> - dHistories.dismiss() + .title(R.string.text_history) + .content(R.string.text_no_history) + .items(WorkingDirectoryUtils.history) + .itemsCallback { dHistory, _, _, text -> + dHistory.dismiss() mContentView.setText(text) } - .itemsLongCallback { dHistories, _, _, text -> + .itemsLongCallback { dHistory, _, _, text -> false.also { MaterialDialog.Builder(prefContext) .title(R.string.text_prompt) @@ -120,11 +120,11 @@ class WorkingDirectoryPreference : MaterialPreference { .positiveColorRes(R.color.dialog_button_caution) .onPositive { ds, _ -> ds.dismiss() - WorkingDirectoryUtils.removeFromHistories(text) - dHistories.items?.let { + WorkingDirectoryUtils.removeFromHistory(text) + dHistory.items?.let { it.remove(text) - dHistories.notifyItemsChanged() - DialogUtils.toggleContentViewByItems(dHistories) + dHistory.notifyItemsChanged() + DialogUtils.toggleContentViewByItems(dHistory) } } .showAdaptive() @@ -133,7 +133,7 @@ class WorkingDirectoryPreference : MaterialPreference { .choiceWidgetThemeColor() .negativeText(R.string.dialog_button_back) .negativeColorRes(R.color.dialog_button_default) - .onNegative { dHistories, _ -> dHistories.dismiss() } + .onNegative { dHistory, _ -> dHistory.dismiss() } .autoDismiss(false) .showAdaptive() .also { DialogUtils.toggleContentViewByItems(it) } @@ -144,7 +144,7 @@ class WorkingDirectoryPreference : MaterialPreference { .positiveColorRes(R.color.dialog_button_attraction) .onPositive { dialog, _ -> val inputPath = mContentView.text.toString() - WorkingDirectoryUtils.addIntoHistories(inputPath) + WorkingDirectoryUtils.addIntoHistory(inputPath) if (isDirPathChanged(inputPath)) { if (mSwitchView.isChecked) { transfer(mPrefFullPath, inputPath) diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/BaseSearchSortListActivity.kt b/app/src/main/java/org/autojs/autojs/ui/storage/BaseSearchSortListActivity.kt new file mode 100644 index 00000000..2c30940c --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/BaseSearchSortListActivity.kt @@ -0,0 +1,305 @@ +package org.autojs.autojs.ui.storage + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.TextView +import androidx.annotation.MenuRes +import androidx.appcompat.widget.SearchView +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.afollestad.materialdialogs.MaterialDialog +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.autojs.autojs.util.DialogUtils +import org.autojs.autojs.theme.widget.ThemeColorSwipeRefreshLayout +import org.autojs.autojs.theme.widget.ThemeColorToolbar +import org.autojs.autojs.ui.BaseActivity +import org.autojs.autojs.util.ViewUtils.excludePaddingClippableViewFromBottomNavigationBar +import org.autojs.autojs.util.ViewUtils.onceGlobalLayout +import org.autojs.autojs.util.ViewUtils.setColorsByThemeColorLuminance +import org.autojs.autojs.util.ViewUtils.setMenuIconsColorByThemeColorLuminance +import org.autojs.autojs.util.ViewUtils.setNavigationIconColorByThemeColorLuminance +import org.autojs.autojs.util.ViewUtils.setTitlesTextColorByThemeColorLuminance +import org.autojs.autojs6.R +import java.util.Locale + +/** + * Base activity for a list page that supports swipe refresh, search, sort, and empty hint. + * zh-CN: 支持下拉刷新/搜索/排序/空态提示的列表页面基类. + * + * Notes. + * zh-CN: 说明. + * 1) This class inflates ViewBinding by subclass factory method. + * zh-CN: 1) 本类通过子类提供的工厂方法创建并持有 ViewBinding. + * 2) Subclass provides view references (toolbar/recycler/swipe/hint) via a holder. + * zh-CN: 2) 子类通过 holder 提供 toolbar/recycler/swipe/hint 等引用. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 5, 2026. + */ +abstract class BaseSearchSortListActivity : BaseActivity() { + + protected data class ListPageViews( + val toolbar: ThemeColorToolbar, + val recyclerView: RecyclerView, + val swipeRefreshLayout: ThemeColorSwipeRefreshLayout, + val emptyHint: TextView, + ) + + protected lateinit var binding: VB + + protected lateinit var toolbar: ThemeColorToolbar + protected lateinit var recyclerView: RecyclerView + protected lateinit var swipeRefreshLayout: ThemeColorSwipeRefreshLayout + protected lateinit var hint: TextView + + protected var searchView: SearchView? = null + + protected var allItems: List = emptyList() + protected var queryText: String = "" + protected var internalSortMode: CommonSortMode = CommonSortMode.TIME_DESC + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = createBinding() + setContentView(binding.root) + + val views = provideViews(binding) + toolbar = views.toolbar + recyclerView = views.recyclerView + swipeRefreshLayout = views.swipeRefreshLayout + hint = views.emptyHint + + recyclerView.apply { + layoutManager = LinearLayoutManager(this@BaseSearchSortListActivity) + adapter = createRecyclerAdapter() + addItemDecoration(DividerItemDecoration(context, VERTICAL)) + excludePaddingClippableViewFromBottomNavigationBar() + } + + swipeRefreshLayout.setOnRefreshListener { load() } + + setToolbarAsBack(getToolbarTitleText()) + + load() + } + + override fun initThemeColors() { + super.initThemeColors() + updateToolbarColors() + updateSearchViewColors() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(menuResId(), menu) + setUpSearchMenuItem(menu) + updateToolbarColors() + updateSearchViewColors() + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val mappedSortMode = sortModeFromMenuItemIdOrNull(item.itemId) + if (mappedSortMode != null) { + setSortMode(mappedSortMode) + return true + } + if (onExtraMenuItemSelected(item)) { + return true + } + return super.onOptionsItemSelected(item) + } + + protected open fun setSortMode(mode: CommonSortMode) { + internalSortMode = mode + applyFilterAndSort() + } + + protected open fun applyFilterAndSort() { + val q = queryText.trim() + val filtered = if (q.isBlank()) { + allItems + } else { + val qLower = q.lowercase(Locale.getDefault()) + allItems.filter { matchesQuery(it, qLower) } + } + + val sorted = sortItems(filtered, internalSortMode) + + updateHintVisibility(sorted) + swipeRefreshLayout.isRefreshing = false + + submitToAdapter(sorted) + updateStats(allItems = allItems, shownItems = sorted) + } + + protected fun load() { + Schedulers.io().scheduleDirect { + runCatching { + loadAllItemsInBackground() + }.onSuccess { items -> + AndroidSchedulers.mainThread().scheduleDirect { + if (isFinishing || isDestroyed) return@scheduleDirect + allItems = items + + // Hook after items loaded (before filter/sort/submit). + // zh-CN: 数据加载完成后的钩子 (在过滤/排序/提交之前调用). + onItemsLoaded(items) + + applyFilterAndSort() + } + }.onFailure { t -> + t.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + if (isFinishing || isDestroyed) return@scheduleDirect + swipeRefreshLayout.isRefreshing = false + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(this) + .title(R.string.text_prompt) + .content(t.message ?: t.toString()) + .positiveText(R.string.dialog_button_dismiss) + .positiveColorRes(R.color.dialog_button_default) + .cancelable(true) + .build() + } + } + } + } + } + + /** + * Called when allItems is updated on main thread, before applyFilterAndSort. + * zh-CN: 当 allItems 在主线程更新后, 且在调用 applyFilterAndSort 之前触发. + */ + protected open fun onItemsLoaded(items: List) = Unit + + protected fun updateHintVisibility(items: List) { + hint.visibility = if (items.isEmpty()) View.VISIBLE else View.GONE + } + + protected fun setUpSearchMenuItem(menu: Menu) { + val searchItem = menu.findItem(R.id.action_search) ?: return + + searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem) = true.also { + toolbar.onceGlobalLayout { updateToolbarColors() } + } + + override fun onMenuItemActionCollapse(item: MenuItem) = true + }) + + val sv = searchItem.actionView as? SearchView + searchView = sv + sv?.queryHint = getString(R.string.text_search) + + sv?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + queryText = query.orEmpty() + applyFilterAndSort() + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + queryText = newText.orEmpty() + applyFilterAndSort() + return true + } + }) + } + + protected fun updateToolbarColors() { + toolbar.setMenuIconsColorByThemeColorLuminance(this) + toolbar.setNavigationIconColorByThemeColorLuminance(this) + toolbar.setTitlesTextColorByThemeColorLuminance(this) + } + + protected fun updateSearchViewColors() { + searchView?.setColorsByThemeColorLuminance(this) + } + + /** + * Create view binding instance. + * zh-CN: 创建 ViewBinding 实例. + */ + protected abstract fun createBinding(): VB + + /** + * Provide view references for list page. + * zh-CN: 提供列表页所需的 view 引用. + */ + protected abstract fun provideViews(binding: VB): ListPageViews + + /** + * Title text for toolbar back mode. + * zh-CN: 返回模式下 toolbar 的标题文本. + */ + protected abstract fun getToolbarTitleText(): String + + /** + * Menu resource id. + * zh-CN: 菜单资源 ID. + */ + @MenuRes + protected abstract fun menuResId(): Int + + /** + * Create RecyclerView adapter. + * zh-CN: 创建 RecyclerView adapter. + */ + protected abstract fun createRecyclerAdapter(): RecyclerView.Adapter<*> + + /** + * Submit items to adapter. + * zh-CN: 提交列表数据到 adapter. + */ + protected abstract fun submitToAdapter(items: List) + + /** + * Load all items on background thread. + * zh-CN: 在后台线程加载全量数据. + */ + protected abstract fun loadAllItemsInBackground(): List + + /** + * Query predicate. + * zh-CN: 查询匹配规则. + */ + protected abstract fun matchesQuery(item: T, qLower: String): Boolean + + /** + * Sort items. + * zh-CN: 对条目排序. + */ + protected abstract fun sortItems(items: List, sortMode: CommonSortMode): List + + /** + * Update toolbar subtitle stats. + * zh-CN: 更新 toolbar 子标题统计信息. + */ + protected abstract fun updateStats(allItems: List, shownItems: List) + + /** + * Map menu item id to sort mode (or null if not a sort item). + * zh-CN: 将菜单项 ID 映射为排序模式 (如果不是排序项则返回 null). + */ + protected open fun sortModeFromMenuItemIdOrNull(itemId: Int): CommonSortMode? = when (itemId) { + R.id.action_sort_time_desc -> CommonSortMode.TIME_DESC + R.id.action_sort_time_asc -> CommonSortMode.TIME_ASC + R.id.action_sort_size_desc -> CommonSortMode.SIZE_DESC + R.id.action_sort_size_asc -> CommonSortMode.SIZE_ASC + R.id.action_sort_name_asc -> CommonSortMode.NAME_ASC + R.id.action_sort_path_asc -> CommonSortMode.PATH_ASC + else -> null + } + + /** + * Handle extra menu items (non-search/non-sort). + * zh-CN: 处理额外菜单项 (非搜索/非排序). + */ + protected open fun onExtraMenuItemSelected(item: MenuItem): Boolean = false +} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/CommonSortMode.kt b/app/src/main/java/org/autojs/autojs/ui/storage/CommonSortMode.kt new file mode 100644 index 00000000..8af87510 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/CommonSortMode.kt @@ -0,0 +1,16 @@ +package org.autojs.autojs.ui.storage + +/** + * Common sort mode for searchable/sortable list pages. + * zh-CN: 可搜索/可排序列表页面的通用排序模式. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 5, 2026. + */ +enum class CommonSortMode { + TIME_DESC, + TIME_ASC, + SIZE_DESC, + SIZE_ASC, + NAME_ASC, + PATH_ASC, +} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/StorageUsageSummaryFormatter.kt b/app/src/main/java/org/autojs/autojs/ui/storage/StorageUsageSummaryFormatter.kt new file mode 100644 index 00000000..14263615 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/StorageUsageSummaryFormatter.kt @@ -0,0 +1,84 @@ +package org.autojs.autojs.ui.storage + +import android.content.Context +import org.autojs.autojs.pio.PFiles +import org.autojs.autojs6.R +import java.util.Locale + +/** + * Storage usage summary formatter for header/subtitle. + * zh-CN: header/subtitle 的容量摘要格式化工具. + * + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 5, 2026. + */ +object StorageUsageSummaryFormatter { + + private const val HEADER_PIPE: String = " | " + private const val SUBTITLE_PIPE: String = " | " + + /** + * Format percent text from bytes and limit. + * zh-CN: 根据已用字节与上限字节格式化百分比文本. + */ + fun formatPercentText(bytes: Long, limitBytes: Long, locale: Locale = Locale.getDefault()): String { + val used = bytes.coerceAtLeast(0L) + val limit = limitBytes.coerceAtLeast(0L) + return if (limit > 0L) { + val ratio = (used.toDouble() / limit.toDouble()).coerceAtLeast(0.0) + String.format(locale, "%.1f%%", ratio * 100.0) + } else { + String.format(locale, "%.1f%%", 0.0) + } + } + + /** + * Format header subtitle text. + * zh-CN: 格式化 header 的子标题文本. + */ + fun formatHeader( + context: Context, + count: Long, + totalBytes: Long, + limitBytes: Long, + ): String { + val countSafe = count.coerceAtLeast(0L) + val bytesSafe = totalBytes.coerceAtLeast(0L) + val limitSafe = limitBytes.coerceAtLeast(0L) + + val itemsPart = context.getString(R.string.text_items_count_colon_value, countSafe) + + val sizeText = PFiles.formatSizeWithUnit(bytesSafe) + val percentText = formatPercentText(bytesSafe, limitSafe) + + var sizePart = context.getString(R.string.text_total_size_colon_value, sizeText) + if (limitSafe > 0L) { + sizePart += " ($percentText)" + } + + return listOf(itemsPart, sizePart).joinToString(HEADER_PIPE) + } + + /** + * Format drawer subtitle text (compact). + * zh-CN: 格式化抽屉子标题文本 (紧凑). + */ + fun formatDrawerSubtitle( + count: Long, + totalBytes: Long, + limitBytes: Long, + locale: Locale = Locale.getDefault(), + ): String { + val countSafe = count.coerceAtLeast(0L) + val bytesSafe = totalBytes.coerceAtLeast(0L) + val limitSafe = limitBytes.coerceAtLeast(0L) + + val sizeText = PFiles.formatSizeWithUnit(bytesSafe) + val percentText = formatPercentText(bytesSafe, limitSafe, locale) + + return listOf( + countSafe.toString(), + sizeText, + percentText, + ).joinToString(SUBTITLE_PIPE) + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt b/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt index e45641e3..0b97fe2d 100644 --- a/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/storage/TrashActivity.kt @@ -2,26 +2,24 @@ package org.autojs.autojs.ui.storage import android.content.Context import android.content.Intent -import android.os.Bundle -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager +import android.view.MenuItem import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog +import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers -import org.autojs.autojs.app.DialogUtils +import org.autojs.autojs.util.DialogUtils +import org.autojs.autojs.core.pref.PrefRx import org.autojs.autojs.storage.history.HistoryDatabase +import org.autojs.autojs.storage.history.HistoryPrefs import org.autojs.autojs.storage.history.TrashEntities +import org.autojs.autojs.storage.history.TrashRepository import org.autojs.autojs.storage.history.TrashRestoreController -import org.autojs.autojs.ui.BaseActivity +import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R +import org.autojs.autojs6.databinding.ActivityTrashBinding import java.io.File -import java.text.SimpleDateFormat -import java.util.Date import java.util.Locale /** @@ -29,68 +27,216 @@ import java.util.Locale * zh-CN: 回收站页面. * * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 5, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. */ -class TrashActivity : BaseActivity() { - - private lateinit var recyclerView: RecyclerView +class TrashActivity : BaseSearchSortListActivity() { private val adapter = TrashAdapter( onClick = { item -> - TrashRestoreController(this).startRestoreFlow(item) - } + val optionMenuItemRemove = MaterialDialog.OptionMenuItemSpec(getString(R.string.text_remove_from_trash)) { d -> + confirmAndRemoveFromTrash(item, d) + } + TrashRestoreController(this).startRestoreFlow(item, listOf(optionMenuItemRemove)) { + load() + } + }, + onLongClick = { item -> + confirmAndRemoveFromTrash(item) + }, ) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + /** + * Disposables for header observing. + * zh-CN: header 监听订阅管理器. + */ + private val headerDisposables: CompositeDisposable = CompositeDisposable() - title = getString(R.string.text_trash) + /** + * Latest computed subtitle from observers. + * zh-CN: 来自监听器的最新 subtitle 文本. + */ + private var observedSubtitle: String? = null - recyclerView = RecyclerView(this).apply { - layoutManager = LinearLayoutManager(this@TrashActivity) - adapter = this@TrashActivity.adapter - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - } - setContentView(recyclerView) - - load() + override fun onStart() { + super.onStart() + startObserveHeaderSubtitleIfNeeded() } - private fun load() { + override fun onStop() { + super.onStop() + // Avoid leaking Activity via subscriptions. + // zh-CN: 避免订阅持有 Activity 导致泄露. + headerDisposables.clear() + } + + private fun startObserveHeaderSubtitleIfNeeded() { + headerDisposables.clear() + + val dao = HistoryDatabase.getInstance(applicationContext).historyDao() + + val limitBytesFlow = PrefRx.observeLong( + keyRes = R.string.key_trash_max_total_bytes, + defaultValue = HistoryPrefs.DEFAULT_TRASH_MAX_TOTAL_BYTES, + ) + + val disposable = Observable + .combineLatest( + dao.observeTrashStats().toObservable(), + limitBytesFlow.toObservable(), + ) { stats, limitBytes -> + StorageUsageSummaryFormatter.formatHeader( + context = this, + count = stats.count, + totalBytes = (stats.totalBytes ?: 0L), + limitBytes = limitBytes, + ) + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { subtitle -> + observedSubtitle = subtitle + toolbar.subtitle = subtitle + } + + headerDisposables.add(disposable) + } + + override fun createBinding(): ActivityTrashBinding { + return ActivityTrashBinding.inflate(layoutInflater) + } + + override fun provideViews(binding: ActivityTrashBinding): ListPageViews { + return ListPageViews( + toolbar = binding.toolbar, + recyclerView = binding.recyclerView, + swipeRefreshLayout = binding.swipeRefreshLayout, + emptyHint = binding.emptyHint, + ) + } + + override fun getToolbarTitleText(): String { + return getString(R.string.text_trash) + } + + override fun menuResId(): Int { + return R.menu.menu_trash + } + + override fun createRecyclerAdapter(): RecyclerView.Adapter<*> { + return adapter + } + + override fun submitToAdapter(items: List) { + adapter.submit(items) + } + + override fun loadAllItemsInBackground(): List { + val dao = HistoryDatabase.getInstance(applicationContext).historyDao() + return dao.listTrashItemsDesc() + } + + override fun matchesQuery(item: TrashEntities.TrashItem, qLower: String): Boolean { + val path = item.originalPath + val name = File(path).name + val locale = Locale.getDefault() + return path.lowercase(locale).contains(qLower) || name.lowercase(locale).contains(qLower) + } + + override fun sortItems( + items: List, + sortMode: CommonSortMode, + ): List { + val locale = Locale.getDefault() + return when (sortMode) { + CommonSortMode.TIME_DESC -> items.sortedByDescending { it.trashedAt } + CommonSortMode.TIME_ASC -> items.sortedBy { it.trashedAt } + + CommonSortMode.SIZE_DESC -> items.sortedByDescending { it.sizeBytes } + CommonSortMode.SIZE_ASC -> items.sortedBy { it.sizeBytes } + + CommonSortMode.NAME_ASC -> items.sortedBy { File(it.originalPath).name.lowercase(locale) } + CommonSortMode.PATH_ASC -> items.sortedBy { it.originalPath.lowercase(locale) } + } + } + + override fun updateStats( + allItems: List, + shownItems: List, + ) { + // Header subtitle is driven by Room observe + Pref observe. + // zh-CN: header subtitle 由 Room 监听 + Pref 监听驱动. + observedSubtitle?.let { toolbar.subtitle = it } + } + + override fun onExtraMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_clear_trash -> true.also { confirmAndClearTrash() } + else -> false + } + } + + private fun confirmAndRemoveFromTrash(item: TrashEntities.TrashItem, parentDialog: MaterialDialog? = null) { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(this) + .title(R.string.text_prompt) + .content(R.string.text_remove_from_trash_confirm) + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { _, _ -> + parentDialog?.dismiss() + removeFromTrash(item) + } + .cancelable(true) + .build() + } + } + + private fun removeFromTrash(item: TrashEntities.TrashItem) { Schedulers.io().scheduleDirect { runCatching { - val dao = HistoryDatabase.getInstance(applicationContext).historyDao() - val items = dao.listTrashItemsDesc() - + TrashRepository(applicationContext).deleteTrashItem(item) AndroidSchedulers.mainThread().scheduleDirect { - adapter.submit(items) - - if (items.isEmpty()) { - DialogUtils.buildAndShowAdaptive { - MaterialDialog.Builder(this) - .title(R.string.text_prompt) - .content(R.string.text_no_data) - .positiveText(R.string.dialog_button_dismiss) - .positiveColorRes(R.color.dialog_button_default) - .cancelable(true) - .build() - } - } + ViewUtils.showToast(this, getString(R.string.text_already_deleted)) + load() } }.onFailure { it.printStackTrace() AndroidSchedulers.mainThread().scheduleDirect { - DialogUtils.buildAndShowAdaptive { - MaterialDialog.Builder(this) - .title(R.string.text_prompt) - .content(it.message ?: it.toString()) - .positiveText(R.string.dialog_button_dismiss) - .positiveColorRes(R.color.dialog_button_default) - .cancelable(true) - .build() - } + ViewUtils.showToast(this, it.message, true) + } + } + } + } + + private fun confirmAndClearTrash() { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(this) + .title(R.string.text_prompt) + .content(R.string.text_clear_trash_confirm) + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { _, _ -> clearTrash() } + .cancelable(true) + .build() + } + } + + private fun clearTrash() { + Schedulers.io().scheduleDirect { + runCatching { + TrashRepository(applicationContext).clearTrash() + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showSnack(recyclerView, getString(R.string.text_done), true) + load() + } + }.onFailure { + it.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showSnack(recyclerView, it.message, true) } } } @@ -104,81 +250,3 @@ class TrashActivity : BaseActivity() { } } } - -private class TrashAdapter( - private val onClick: (TrashEntities.TrashItem) -> Unit, -) : RecyclerView.Adapter() { - - private var items: List = emptyList() - - fun submit(newItems: List) { - items = newItems - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrashViewHolder { - return TrashViewHolder.create(parent, onClick) - } - - override fun onBindViewHolder(holder: TrashViewHolder, position: Int) { - holder.bind(items[position]) - } - - override fun getItemCount(): Int = items.size -} - -private class TrashViewHolder( - itemView: View, - private val onClick: (TrashEntities.TrashItem) -> Unit, -) : RecyclerView.ViewHolder(itemView) { - - private val titleView: TextView = (itemView as LinearLayout).getChildAt(0) as TextView - private val subtitleView: TextView = (itemView as LinearLayout).getChildAt(1) as TextView - - private var boundItem: TrashEntities.TrashItem? = null - - fun bind(item: TrashEntities.TrashItem) { - boundItem = item - - val name = File(item.originalPath).name - titleView.text = name - - val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - val timeText = fmt.format(Date(item.trashedAt)) - subtitleView.text = "${item.originalPath}\n$timeText" - - itemView.setOnClickListener { - boundItem?.let(onClick) - } - } - - companion object { - fun create(parent: ViewGroup, onClick: (TrashEntities.TrashItem) -> Unit): TrashViewHolder { - val ctx = parent.context - - val root = LinearLayout(ctx).apply { - orientation = LinearLayout.VERTICAL - setPadding(32, 24, 32, 24) - layoutParams = RecyclerView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - gravity = Gravity.CENTER_VERTICAL - } - - val title = TextView(ctx).apply { - textSize = 16f - } - - val subtitle = TextView(ctx).apply { - textSize = 12f - alpha = 0.75f - } - - root.addView(title) - root.addView(subtitle) - - return TrashViewHolder(root, onClick) - } - } -} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/TrashAdapter.kt b/app/src/main/java/org/autojs/autojs/ui/storage/TrashAdapter.kt new file mode 100644 index 00000000..0297c323 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/TrashAdapter.kt @@ -0,0 +1,34 @@ +package org.autojs.autojs.ui.storage + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.autojs.autojs.storage.history.TrashEntities +import org.autojs.autojs6.databinding.ItemTrashBinding + +/** + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + */ +internal class TrashAdapter( + private val onClick: (TrashEntities.TrashItem) -> Unit, + private val onLongClick: (TrashEntities.TrashItem) -> Unit, +) : RecyclerView.Adapter() { + + private var items: List = emptyList() + + fun submit(newItems: List) { + items = newItems + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrashViewHolder { + val binding = ItemTrashBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return TrashViewHolder(binding, onClick, onLongClick) + } + + override fun onBindViewHolder(holder: TrashViewHolder, position: Int) { + holder.bind(items[position]) + } + + override fun getItemCount(): Int = items.size +} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/TrashViewHolder.kt b/app/src/main/java/org/autojs/autojs/ui/storage/TrashViewHolder.kt new file mode 100644 index 00000000..850e756e --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/TrashViewHolder.kt @@ -0,0 +1,57 @@ +package org.autojs.autojs.ui.storage + +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import org.autojs.autojs.pio.PFiles +import org.autojs.autojs.storage.history.TrashEntities +import org.autojs.autojs.util.StringUtils.normalizeTrailingSlash +import org.autojs.autojs6.R +import org.autojs.autojs6.databinding.ItemTrashBinding +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + */ +internal class TrashViewHolder( + binding: ItemTrashBinding, + private val onClick: (TrashEntities.TrashItem) -> Unit, + private val onLongClick: (TrashEntities.TrashItem) -> Unit, +) : RecyclerView.ViewHolder(binding.root) { + + private val iconView: ImageView = binding.icon + private val titleView: TextView = binding.title + private val subtitleView: TextView = binding.subtitle + + private var boundItem: TrashEntities.TrashItem? = null + + fun bind(item: TrashEntities.TrashItem) { + boundItem = item + + iconView.setImageResource(if (item.isDirectory) R.drawable.ic_simple_folder else R.drawable.ic_simple_file) + titleView.text = File(item.originalPath).name + + val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val timeText = fmt.format(Date(item.trashedAt)) + val sizeText = PFiles.formatSizeWithUnit(item.sizeBytes) + + subtitleView.text = buildString { + append(timeText) + append(" | ") + append(sizeText) + append("\n") + append(item.originalPath.normalizeTrailingSlash(item.isDirectory)) + } + + itemView.setOnClickListener { + boundItem?.let(onClick) + } + itemView.setOnLongClickListener { + boundItem?.let(onLongClick) + true + } + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt index 4c54d2c0..86088f4f 100644 --- a/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt +++ b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryActivity.kt @@ -2,94 +2,389 @@ package org.autojs.autojs.ui.storage import android.content.Context import android.content.Intent -import android.os.Bundle -import android.view.Gravity -import android.view.View -import android.view.ViewGroup -import android.widget.LinearLayout -import android.widget.TextView -import androidx.recyclerview.widget.LinearLayoutManager +import android.view.MenuItem import androidx.recyclerview.widget.RecyclerView import com.afollestad.materialdialogs.MaterialDialog +import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers -import org.autojs.autojs.app.DialogUtils +import org.autojs.autojs.util.DialogUtils +import org.autojs.autojs.core.pref.PrefRx import org.autojs.autojs.storage.history.HistoryDatabase import org.autojs.autojs.storage.history.HistoryEntities +import org.autojs.autojs.storage.history.HistoryPrefs +import org.autojs.autojs.storage.history.HistoryRepository import org.autojs.autojs.storage.history.VersionHistoryController -import org.autojs.autojs.ui.BaseActivity +import org.autojs.autojs.util.ViewUtils import org.autojs.autojs6.R -import java.text.SimpleDateFormat -import java.util.Date +import org.autojs.autojs6.databinding.ActivityVersionHistoryBinding +import java.io.File import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicBoolean /** * Version history page (all files). * zh-CN: 版本历史页面 (全部文件). * * Created by JetBrains AI Assistant (GPT-5.2) on Feb 4, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 6, 2026. */ -class VersionHistoryActivity : BaseActivity() { +class VersionHistoryActivity : BaseSearchSortListActivity() { - private lateinit var recyclerView: RecyclerView + /** + * Cached aggregated bytes for fileId. + * zh-CN: fileId 对应的聚合字节数缓存. + */ + private val sizeCache: ConcurrentHashMap = ConcurrentHashMap() + + /** + * Cached revision count for fileId. + * zh-CN: fileId 对应的 revision 数量缓存. + */ + private val countCache: ConcurrentHashMap = ConcurrentHashMap() + + /** + * Whether caches have been populated once. + * zh-CN: 缓存是否已至少完成一次填充. + */ + private val sizeReady: AtomicBoolean = AtomicBoolean(false) + + /** + * Prevent duplicated async loading. + * zh-CN: 防止重复触发异步加载. + */ + private val sizeLoading: AtomicBoolean = AtomicBoolean(false) private val adapter = VersionHistoryFileAdapter( onClick = { entry -> - VersionHistoryController(this).showForFilePath(entry.logicalPath) - } + VersionHistoryController(this).showForFilePath(entry.canonicalPath, entry.displayPath) + }, + onLongClick = { entry -> + confirmAndClearHistoryForEntry(entry) + }, + getSizeBytesOrNull = { fileId -> + sizeCache[fileId] + }, + getRevisionCountOrNull = { fileId -> + countCache[fileId] + }, ) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + /** + * Disposables for header observing. + * zh-CN: header 监听订阅管理器. + */ + private val headerDisposables: CompositeDisposable = CompositeDisposable() - title = getString(R.string.text_version_history) + /** + * Latest computed subtitle from observers. + * zh-CN: 来自监听器的最新 subtitle 文本. + */ + private var observedSubtitle: String? = null - recyclerView = RecyclerView(this).apply { - layoutManager = LinearLayoutManager(this@VersionHistoryActivity) - adapter = this@VersionHistoryActivity.adapter - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - } - setContentView(recyclerView) - - load() + override fun onStart() { + super.onStart() + startObserveHeaderSubtitleIfNeeded() } - private fun load() { + override fun onStop() { + super.onStop() + // Avoid leaking Activity via subscriptions. + // zh-CN: 避免订阅持有 Activity 导致泄露. + headerDisposables.clear() + } + + private fun startObserveHeaderSubtitleIfNeeded() { + headerDisposables.clear() + + val dao = HistoryDatabase.getInstance(applicationContext).historyDao() + + val limitBytesFlow = PrefRx.observeLong( + keyRes = R.string.key_history_max_total_bytes, + defaultValue = HistoryPrefs.DEFAULT_HISTORY_MAX_TOTAL_BYTES, + ) + + val disposable = Observable + .combineLatest( + dao.observeVersionHistoryStats().toObservable(), + limitBytesFlow.toObservable(), + ) { stats, limitBytes -> + StorageUsageSummaryFormatter.formatHeader( + context = this, + count = stats.fileCount, + totalBytes = (stats.totalBytes ?: 0L), + limitBytes = limitBytes, + ) + } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { subtitle -> + observedSubtitle = subtitle + toolbar.subtitle = subtitle + } + + headerDisposables.add(disposable) + } + + override fun createBinding(): ActivityVersionHistoryBinding { + return ActivityVersionHistoryBinding.inflate(layoutInflater) + } + + override fun provideViews(binding: ActivityVersionHistoryBinding): ListPageViews { + return ListPageViews( + toolbar = binding.toolbar, + recyclerView = binding.recyclerView, + swipeRefreshLayout = binding.swipeRefreshLayout, + emptyHint = binding.emptyHint, + ) + } + + override fun getToolbarTitleText(): String { + return getString(R.string.text_version_history) + } + + override fun menuResId(): Int { + return R.menu.menu_version_history + } + + override fun createRecyclerAdapter(): RecyclerView.Adapter<*> { + return adapter + } + + override fun submitToAdapter(items: List) { + adapter.submit(items) + } + + override fun loadAllItemsInBackground(): List { + val dao = HistoryDatabase.getInstance(applicationContext).historyDao() + + // Default sorting: latest seen first. + // zh-CN: 默认排序: 最近访问优先. + return dao.listAllFiles().sortedByDescending { it.lastSeenAt } + } + + override fun onItemsLoaded(items: List) { + // Recompute stats on each load (first enter / swipe refresh). + // zh-CN: 每次 load 后都重新计算聚合统计 (首次进入/下拉刷新). + sizeCache.clear() + countCache.clear() + sizeReady.set(false) + sizeLoading.set(false) + + // Refresh per-row display immediately (show "Calculating..."). + // zh-CN: 立即刷新每行显示 (展示 "Calculating..."). + adapter.notifyDataSetChanged() + + ensureStatsLoadedAsync(forceRecompute = true) + } + + override fun onExtraMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_clear_version_history -> true.also { confirmAndClearAllHistory() } + else -> false + } + } + + override fun setSortMode(mode: CommonSortMode) { + internalSortMode = mode + + val needSize = mode == CommonSortMode.SIZE_DESC || mode == CommonSortMode.SIZE_ASC + if (needSize && !sizeReady.get()) { + // Strict mode: wait for real size before sorting by size. + // zh-CN: 严格模式: size 未就绪时, 切到按大小排序需等待真实 size 后再排序. + swipeRefreshLayout.isRefreshing = true + ensureStatsLoadedAsync(forceRecompute = false) + updateStats(allItems = allItems, shownItems = emptyList()) + return + } + + applyFilterAndSort() + } + + /** + * Ensure aggregated stats (size + count) loaded. + * zh-CN: 确保聚合统计 (size + count) 已加载. + */ + private fun ensureStatsLoadedAsync(forceRecompute: Boolean) { + if (!forceRecompute && sizeReady.get()) return + if (!sizeLoading.compareAndSet(false, true)) return + Schedulers.io().scheduleDirect { runCatching { val dao = HistoryDatabase.getInstance(applicationContext).historyDao() - val files = dao.listAllFiles().sortedByDescending { it.lastSeenAt } + val fileIds = allItems.map { it.fileId } + val rows = if (fileIds.isEmpty()) emptyList() else dao.aggStatsByFileIds(fileIds) + val sizeMap = HashMap(rows.size) + val countMap = HashMap(rows.size) + + for (row in rows) { + sizeMap[row.fileId] = (row.totalBytes ?: 0L).coerceAtLeast(0L) + countMap[row.fileId] = row.revisionCount.coerceAtLeast(0L) + } + + Pair(sizeMap, countMap) + }.onSuccess { (sizeMap, countMap) -> AndroidSchedulers.mainThread().scheduleDirect { - adapter.submit(files) + if (isFinishing || isDestroyed) return@scheduleDirect - if (files.isEmpty()) { - DialogUtils.buildAndShowAdaptive { - MaterialDialog.Builder(this) - .title(R.string.text_prompt) - .content(R.string.text_no_data) - .positiveText(R.string.dialog_button_dismiss) - .positiveColorRes(R.color.dialog_button_default) - .cancelable(true) - .build() - } + // Replace caches with newly computed values. + // zh-CN: 使用新计算结果替换缓存. + sizeCache.clear() + sizeCache.putAll(sizeMap) + + countCache.clear() + countCache.putAll(countMap) + + sizeReady.set(true) + sizeLoading.set(false) + + // Refresh per-row display. + // zh-CN: 刷新每行显示. + adapter.notifyDataSetChanged() + + // If current mode is size sort, now apply strict sorting. + // zh-CN: 若当前为按大小排序, 则此时才允许应用严格排序. + val needSize = internalSortMode == CommonSortMode.SIZE_DESC || internalSortMode == CommonSortMode.SIZE_ASC + if (needSize) { + applyFilterAndSort() + } else { + swipeRefreshLayout.isRefreshing = false + updateStats(allItems = allItems, shownItems = emptyList()) } } + }.onFailure { t -> + t.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + sizeLoading.set(false) + swipeRefreshLayout.isRefreshing = false + + // Keep UI usable even if stats calculation failed. + // zh-CN: 统计计算失败时仍保持 UI 可用. + applyFilterAndSort() + } + } + } + } + + override fun matchesQuery(item: HistoryEntities.FileEntry, qLower: String): Boolean { + val path = item.displayPath + val name = File(path).name + val locale = Locale.getDefault() + return path.lowercase(locale).contains(qLower) || name.lowercase(locale).contains(qLower) + } + + override fun sortItems( + items: List, + sortMode: CommonSortMode, + ): List { + val locale = Locale.getDefault() + return when (sortMode) { + CommonSortMode.TIME_DESC -> items.sortedByDescending { it.lastSeenAt } + CommonSortMode.TIME_ASC -> items.sortedBy { it.lastSeenAt } + + CommonSortMode.SIZE_DESC -> items.sortedWith( + compareByDescending { sizeCache[it.fileId] ?: 0L } + .thenBy { it.canonicalPath.lowercase(locale) } + ) + + CommonSortMode.SIZE_ASC -> items.sortedWith( + compareBy { sizeCache[it.fileId] ?: 0L } + .thenBy { it.canonicalPath.lowercase(locale) } + ) + + CommonSortMode.NAME_ASC -> items.sortedBy { File(it.canonicalPath).name.lowercase(locale) } + CommonSortMode.PATH_ASC -> items.sortedBy { it.canonicalPath.lowercase(locale) } + } + } + + override fun updateStats( + allItems: List, + shownItems: List, + ) { + // Header subtitle is driven by Room observe + Pref observe. + // zh-CN: header subtitle 由 Room 监听 + Pref 监听驱动. + observedSubtitle?.let { toolbar.subtitle = it } + } + + /** + * Confirm clearing all history. + * zh-CN: 确认清空全部历史记录. + */ + private fun confirmAndClearAllHistory() { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(this) + .title(R.string.text_prompt) + .content(R.string.text_clear_version_history_confirm) + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { _, _ -> clearAllHistory() } + .cancelable(true) + .build() + } + } + + /** + * Clear all history in background. + * zh-CN: 在后台线程清空全部历史记录. + */ + private fun clearAllHistory() { + Schedulers.io().scheduleDirect { + runCatching { + HistoryRepository(applicationContext).clearAllHistory() + }.onSuccess { + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(this, getString(R.string.text_history_has_been_cleared), true) + load() + } }.onFailure { it.printStackTrace() AndroidSchedulers.mainThread().scheduleDirect { - DialogUtils.buildAndShowAdaptive { - MaterialDialog.Builder(this) - .title(R.string.text_prompt) - .content(it.message ?: it.toString()) - .positiveText(R.string.dialog_button_dismiss) - .positiveColorRes(R.color.dialog_button_default) - .cancelable(true) - .build() - } + ViewUtils.showToast(this, it.message, true) + } + } + } + } + + /** + * Confirm clearing history for one file entry (all revisions). + * zh-CN: 确认清除单个条目对应的历史记录 (全部 revisions). + */ + private fun confirmAndClearHistoryForEntry(entry: HistoryEntities.FileEntry) { + DialogUtils.buildAndShowAdaptive { + MaterialDialog.Builder(this) + .title(R.string.text_prompt) + .content(getString(R.string.text_confirm_to_clear_the_history_of, entry.canonicalPath)) + .negativeText(R.string.dialog_button_cancel) + .negativeColorRes(R.color.dialog_button_default) + .positiveText(R.string.dialog_button_confirm) + .positiveColorRes(R.color.dialog_button_caution) + .onPositive { _, _ -> clearHistoryForEntry(entry) } + .cancelable(true) + .build() + } + } + + /** + * Clear history for one entry in background. + * zh-CN: 在后台线程清除单个条目的历史记录. + */ + private fun clearHistoryForEntry(entry: HistoryEntities.FileEntry) { + Schedulers.io().scheduleDirect { + runCatching { + HistoryRepository(applicationContext).clearHistoryForPath(entry.canonicalPath) + }.onSuccess { + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(this, getString(R.string.text_history_has_been_cleared), true) + load() + } + }.onFailure { + it.printStackTrace() + AndroidSchedulers.mainThread().scheduleDirect { + ViewUtils.showToast(this, it.message, true) } } } @@ -103,80 +398,3 @@ class VersionHistoryActivity : BaseActivity() { } } } - -private class VersionHistoryFileAdapter( - private val onClick: (HistoryEntities.FileEntry) -> Unit, -) : RecyclerView.Adapter() { - - private var items: List = emptyList() - - fun submit(newItems: List) { - items = newItems - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VersionHistoryFileViewHolder { - return VersionHistoryFileViewHolder.create(parent, onClick) - } - - override fun onBindViewHolder(holder: VersionHistoryFileViewHolder, position: Int) { - holder.bind(items[position]) - } - - override fun getItemCount(): Int = items.size -} - -private class VersionHistoryFileViewHolder( - itemView: View, - private val onClick: (HistoryEntities.FileEntry) -> Unit, -) : RecyclerView.ViewHolder(itemView) { - - private val titleView: TextView = (itemView as LinearLayout).getChildAt(0) as TextView - private val subtitleView: TextView = (itemView as LinearLayout).getChildAt(1) as TextView - - private var boundItem: HistoryEntities.FileEntry? = null - - fun bind(item: HistoryEntities.FileEntry) { - boundItem = item - - titleView.text = item.logicalPath - - val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - val timeText = fmt.format(Date(item.lastSeenAt)) - subtitleView.text = "${item.latestFingerprint}\n$timeText" - - itemView.setOnClickListener { - boundItem?.let(onClick) - } - } - - companion object { - fun create(parent: ViewGroup, onClick: (HistoryEntities.FileEntry) -> Unit): VersionHistoryFileViewHolder { - val ctx = parent.context - - val root = LinearLayout(ctx).apply { - orientation = LinearLayout.VERTICAL - setPadding(32, 24, 32, 24) - layoutParams = RecyclerView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - ) - gravity = Gravity.CENTER_VERTICAL - } - - val title = TextView(ctx).apply { - textSize = 15f - } - - val subtitle = TextView(ctx).apply { - textSize = 11f - alpha = 0.75f - } - - root.addView(title) - root.addView(subtitle) - - return VersionHistoryFileViewHolder(root, onClick) - } - } -} diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileAdapter.kt b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileAdapter.kt new file mode 100644 index 00000000..54ae2678 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileAdapter.kt @@ -0,0 +1,38 @@ +package org.autojs.autojs.ui.storage + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.autojs.autojs.storage.history.HistoryEntities + +/** + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 5, 2026. + */ +internal class VersionHistoryFileAdapter( + private val onClick: (HistoryEntities.FileEntry) -> Unit, + private val onLongClick: (HistoryEntities.FileEntry) -> Unit, + private val getSizeBytesOrNull: (fileId: String) -> Long?, + private val getRevisionCountOrNull: (fileId: String) -> Long?, +) : RecyclerView.Adapter() { + + private var items: List = emptyList() + + fun submit(newItems: List) { + items = newItems + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VersionHistoryFileViewHolder { + return VersionHistoryFileViewHolder.create(parent, onClick, onLongClick) + } + + override fun onBindViewHolder(holder: VersionHistoryFileViewHolder, position: Int) { + val item = items[position] + holder.bind( + item = item, + totalSizeBytesOrNull = getSizeBytesOrNull(item.fileId), + revisionCountOrNull = getRevisionCountOrNull(item.fileId), + ) + } + + override fun getItemCount(): Int = items.size +} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileViewHolder.kt b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileViewHolder.kt new file mode 100644 index 00000000..718ae573 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/storage/VersionHistoryFileViewHolder.kt @@ -0,0 +1,123 @@ +package org.autojs.autojs.ui.storage + +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import org.autojs.autojs.pio.PFiles +import org.autojs.autojs.storage.history.HistoryEntities +import org.autojs.autojs6.R +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Created by JetBrains AI Assistant (GPT-5.2) on Feb 5, 2026. + * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 6, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. + */ +internal class VersionHistoryFileViewHolder( + itemView: View, + private val onClick: (HistoryEntities.FileEntry) -> Unit, + private val onLongClick: (HistoryEntities.FileEntry) -> Unit, +) : RecyclerView.ViewHolder(itemView) { + + private val titleView: TextView = (itemView as LinearLayout).getChildAt(0) as TextView + private val subtitleView: TextView = (itemView as LinearLayout).getChildAt(1) as TextView + + private var boundItem: HistoryEntities.FileEntry? = null + + fun bind( + item: HistoryEntities.FileEntry, + totalSizeBytesOrNull: Long?, + revisionCountOrNull: Long?, + ) { + boundItem = item + + titleView.text = File(item.displayPath).name + + val context = itemView.context + + val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val timeText = fmt.format(Date(item.lastSeenAt)) + + val sizeText = totalSizeBytesOrNull + ?.coerceAtLeast(0L)?.let { PFiles.formatSizeWithUnit(it) } + ?: context.getString(R.string.text_half_ellipsis) + + val countText = revisionCountOrNull + ?.coerceAtLeast(0L)?.toString() + ?: context.getString(R.string.text_half_ellipsis) + + subtitleView.text = buildString { + append(timeText) + append(" | ") + append("< $countText >") + append(" | ") + append(sizeText) + append("\n") + append(item.displayPath) + } + + itemView.setOnClickListener { + boundItem?.let(onClick) + } + itemView.setOnLongClickListener { + boundItem?.let(onLongClick) + true + } + } + + companion object { + + fun create( + parent: ViewGroup, + onClick: (HistoryEntities.FileEntry) -> Unit, + onLongClick: (HistoryEntities.FileEntry) -> Unit, + ): VersionHistoryFileViewHolder { + val ctx = parent.context + + val root = LinearLayout(ctx).apply { + orientation = LinearLayout.VERTICAL + setPadding(32, 24, 32, 24) + layoutParams = RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + gravity = Gravity.CENTER_VERTICAL + + // Use theme selectable background to show press/ripple feedback. + // zh-CN: 使用主题的可选择背景, 显示按压态或 ripple 点击反馈. + with(ctx.theme) { + val typedValue = TypedValue() + resolveAttribute(android.R.attr.selectableItemBackground, typedValue, true) + setBackgroundResource(typedValue.resourceId) + } + + // Ensure this view can show pressed state. + // zh-CN: 确保该 view 能显示 pressed 状态. + isClickable = true + isFocusable = true + isLongClickable = true + } + + val title = TextView(ctx).apply { + textSize = 16f + } + + val subtitle = TextView(ctx).apply { + textSize = 12f + alpha = 0.75f + } + + root.addView(title) + root.addView(subtitle) + + return VersionHistoryFileViewHolder(root, onClick, onLongClick) + } + } +} diff --git a/app/src/main/java/org/autojs/autojs/app/DialogUtils.kt b/app/src/main/java/org/autojs/autojs/util/DialogUtils.kt similarity index 59% rename from app/src/main/java/org/autojs/autojs/app/DialogUtils.kt rename to app/src/main/java/org/autojs/autojs/util/DialogUtils.kt index b65bf11c..69ac28bd 100644 --- a/app/src/main/java/org/autojs/autojs/app/DialogUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/DialogUtils.kt @@ -1,6 +1,4 @@ -@file:Suppress("unused") - -package org.autojs.autojs.app +package org.autojs.autojs.util import android.app.Activity import android.content.Context @@ -9,6 +7,7 @@ import android.content.DialogInterface import android.content.res.ColorStateList import android.os.Build import android.os.Looper +import android.os.SystemClock import android.provider.Settings import android.text.util.Linkify import android.util.Log @@ -16,35 +15,62 @@ import android.view.Gravity import android.view.View import android.view.WindowManager import android.widget.CheckBox +import android.widget.TextView import androidx.preference.PreferenceViewHolder import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.autojs.autojs.annotation.ReservedForCompatibility +import org.autojs.autojs.app.GlobalAppContext import org.autojs.autojs.event.BackCompat +import org.autojs.autojs.theme.ThemeColorHelper import org.autojs.autojs.theme.ThemeColorManager import org.autojs.autojs.theme.preference.LongClickablePreferenceLike import org.autojs.autojs.ui.explorer.ExplorerView -import org.autojs.autojs.util.ColorUtils -import org.autojs.autojs.util.ThreadUtils.runOnMain -import org.autojs.autojs.util.ViewUtils.showSnack +import org.autojs.autojs.util.ViewUtils.setLinesEllipsizedIndividually import org.autojs.autojs6.R import java.util.Locale import java.util.concurrent.Callable import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference +import kotlin.concurrent.Volatile +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt /** * Created by Stardust on Aug 4, 2017. * Transformed by SuperMonster003 on Oct 19, 2022. * Modified by OpenAI ChatGPT (GPT-5.2 Thinking) as of Jan 20, 2026. * Modified by JetBrains AI Assistant (GPT-5.2) as of Feb 1, 2026. - * Modified by SuperMonster003 as of Feb 3, 2026. + * Modified by SuperMonster003 as of Feb 7, 2026. */ object DialogUtils { private const val TAG = "DialogUtils" + // Minimum UI update interval for progress dialog, to reduce main-thread pressure. + // zh-CN: 进度对话框 UI 更新的最小间隔, 用于降低主线程压力. + private const val PROGRESS_UPDATE_MIN_INTERVAL_MS: Long = 50L + + // Delay before showing progress dialog to avoid flashing for fast operations. + // zh-CN: 显示进度对话框前的延迟, 用于避免快速操作导致的闪现. + private const val PROGRESS_DIALOG_SHOW_DELAY_MS: Long = 200L + + // Minimum duration to keep progress dialog visible once shown, to avoid flashing. + // zh-CN: 进度对话框一旦显示后的最短展示时长, 用于避免闪现. + private const val PROGRESS_DIALOG_MIN_SHOW_MS: Long = 300L + + // Progress max for large files (totalBytes > Integer.MAX_VALUE), higher means smoother progress. + // zh-CN: 大文件 (totalBytes > Integer.MAX_VALUE) 的进度最大值, 值越大进度越细腻. + const val PROGRESS_MAX_LARGE: Int = 10000 + @JvmStatic fun T.showAdaptive(): MaterialDialog = build().showAdaptive() @@ -60,7 +86,7 @@ object DialogUtils { * * Behavior: * 1) Always performs window operations and `show()` on the main thread. - * 2) If the dialog uses an [Activity] context, it is shown normally. + * 2) If the dialog uses an [android.app.Activity] context, it is shown normally. * 3) Otherwise, it tries to show as an overlay window: * - `TYPE_APPLICATION_OVERLAY` on Android O+. * - `TYPE_PHONE` on pre-O devices. @@ -72,7 +98,7 @@ object DialogUtils { * * 行为说明: * 1) 所有 Window 参数操作和 `show()` 都保证在主线程执行. - * 2) 若对话框基于 [Activity] Context, 则按常规方式显示. + * 2) 若对话框基于 [android.app.Activity] Context, 则按常规方式显示. * 3) 否则尝试以 overlay 窗口显示: * - Android O+ 使用 `TYPE_APPLICATION_OVERLAY`. * - Android O 以下使用 `TYPE_PHONE`. @@ -96,7 +122,7 @@ object DialogUtils { @Deprecated("Use showAdaptive instead.", ReplaceWith("showAdaptive(dialog, focusable)")) @ReservedForCompatibility fun showDialog(dialog: T, focusable: Boolean = true): T { - runOnMain { + ThreadUtils.runOnMain { // Prevent duplicated show. // zh-CN: 防止重复 show(). if (dialog.isShowing) return@runOnMain @@ -300,13 +326,90 @@ object DialogUtils { } } + fun MaterialDialog.makeSettingsLaunchable(viewGetter: (dialog: MaterialDialog) -> View?, packageName: String?) { + viewGetter(this)?.setOnClickListener { + packageName?.let { pkg -> + IntentUtils.launchAppDetailsSettings(this.context, pkg) + } ?: ViewUtils.showSnack(this.view, R.string.error_app_not_installed) + } + } + + @JvmStatic + fun MaterialDialog.makeTextCopyable(textViewGetter: (dialog: MaterialDialog) -> TextView?) { + makeTextCopyable(textViewGetter(this)) + } + + @JvmStatic + @JvmOverloads + fun MaterialDialog.makeTextCopyable(textView: TextView?, textValue: String? = textView?.text?.toString()) { + if (!textValue.isNullOrBlank()) { + val context = this.context + textView?.setOnClickListener { + ClipboardUtils.setClip(context, textValue) + ViewUtils.showSnack(this.view, "${context.getString(R.string.text_already_copied_to_clip)}: $textValue") + } + } + } + + fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, textValuePair: Pair) { + setCopyableTextIfAbsent(textView, textValuePair.first, textValuePair.second) + } + + fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, textValue: String?, suffix: String? = null) { + when { + textValue.isNullOrBlank() -> { + if (textView.isNullOrBlank() || textView.isEllipsisSix()) { + textView.setUnknown() + } + } + textView.isNullOrBlank() || textView.isEllipsisSix() || textView.isUnknown() -> { + textView.text = textValue + (suffix ?: "") + this.makeTextCopyable(textView, textValue) + } + } + } + + fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, scope: CoroutineScope, f: () -> String?) { + setCopyableTextIfAbsent(textView, scope, f, suffix = null) + } + + fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, scope: CoroutineScope, f: () -> String?, suffix: String?) { + scope.launch(Dispatchers.IO) { + val textValue = f.invoke() + withContext(Dispatchers.Main) { + setCopyableTextIfAbsent(textView, textValue, suffix) + } + } + } + + fun MaterialDialog.setCopyableText(textView: TextView, textValue: String?) { + when { + textValue.isNullOrBlank() -> { + textView.setUnknown() + } + else -> { + textView.text = textValue + this.makeTextCopyable(textView, textValue) + } + } + } + + @JvmStatic + fun MaterialDialog.Builder.choiceWidgetThemeColor() = also { + it.choiceWidgetColor(ThemeColorHelper.getThemeColorStateList(context)) + } + + @JvmStatic + fun MaterialDialog.Builder.widgetThemeColor() = also { + it.widgetColor(ThemeColorHelper.getThemeColorStateList(context)) + } + @JvmStatic fun fixCheckBoxGravity(dialog: MaterialDialog): MaterialDialog = dialog.also { it.view.findViewById(com.afollestad.materialdialogs.R.id.md_promptCheckbox)?.gravity = Gravity.CENTER_VERTICAL } - @JvmStatic - fun isActivityContext(context: Context?): Boolean { + private fun isActivityContext(context: Context?): Boolean { return context is Activity || (context is ContextWrapper && isActivityContext(context.baseContext)) } @@ -345,7 +448,7 @@ object DialogUtils { val now = System.currentTimeMillis() if (now - time.lastPressed >= time.minPressInterval) { time.lastPressed = now - showSnack(explorerView, R.string.text_press_again_to_dismiss_dialog) + ViewUtils.showSnack(explorerView, R.string.text_press_again_to_dismiss_dialog) return@installDialogBackHandler true } di.dismiss() @@ -496,4 +599,212 @@ object DialogUtils { fun MaterialDialog.setActionButtonText(actionButton: DialogAction, string: String) { getActionButton(actionButton).text = string } -} \ No newline at end of file + + class ProgressDialogSession internal constructor(private val controller: OperationController) { + private val dialogRef = AtomicReference() + + // Whether the session has been closed (operation finished/aborted), used to prevent late dialog showing. + // zh-CN: 会话是否已关闭 (操作已完成/已中止), 用于防止延迟任务在结束后仍弹出对话框. + private val closed = AtomicBoolean(false) + + private val showScheduled = AtomicBoolean(false) + + @Volatile + private var shown = false + + private var shownAtUptimeMs = 0L + + // Pending UI state before dialog is actually shown. + // zh-CN: 对话框真正 show 之前的挂起 UI 状态, 用于避免延迟显示导致的首次更新丢失. + private val pendingContentRef: AtomicReference?> = AtomicReference(null) + private val pendingProcessedRef: AtomicLong = AtomicLong(-1L) + private val pendingTotalRef: AtomicLong = AtomicLong(0L) + + // Timestamp/progress/content state for throttling UI updates. + // zh-CN: 用于节流 UI 更新的时间戳/进度/文本状态. + private val lastProgressUpdateTime: LongArray = longArrayOf(0L) + private val lastProgressValue: LongArray = longArrayOf(-1L) + private val lastContentUpdateTime: LongArray = longArrayOf(0L) + + fun scheduleShow(builder: MaterialDialog.Builder) { + if (!showScheduled.compareAndSet(false, true)) { + return + } + + GlobalAppContext.postDelayed(Runnable { + // Skip showing if operation already finished/aborted. + // zh-CN: 若操作已结束/已中止, 则不再显示对话框. + if (closed.get() || controller.cancelled.get()) { + return@Runnable + } + + val dialog: MaterialDialog = buildAndShowAdaptive { builder.build() } + dialog.setProgressNumberFormat("...") + dialog.applyProgressThemeColorTintLists() + + // If closed becomes true right after building, dismiss immediately to avoid dangling dialog. + // zh-CN: 若 build 后立刻变为 closed, 则立即关闭, 避免对话框悬挂. + if (closed.get()) { + try { + if (dialog.isShowing) { + dialog.dismiss() + } + } catch (_: Throwable) { + /* Ignored. */ + } + return@Runnable + } + + dialogRef.set(dialog) + shown = true + shownAtUptimeMs = SystemClock.uptimeMillis() + + // Apply pending content/progress immediately after dialog is ready. + // zh-CN: 对话框就绪后立即应用挂起的 content/progress, 避免首次更新被延迟显示吞掉. + pendingContentRef.getAndSet(null)?.let { pending -> + GlobalAppContext.post { + try { + dialog.contentView?.setLinesEllipsizedIndividually(pending, 1.5f) + } catch (_: Throwable) { + /* Ignored. */ + } + } + } + + val pendingProcessed = pendingProcessedRef.getAndSet(-1L) + if (pendingProcessed >= 0L) { + val pendingTotal = pendingTotalRef.get() + setProgressThrottled(pendingProcessed, pendingTotal) + } + }, PROGRESS_DIALOG_SHOW_DELAY_MS) + } + + fun setContentThrottled(contentList: MutableList) { + val dialog = dialogRef.get() + if (dialog == null) { + // Cache the latest content if dialog is not shown yet. + // zh-CN: 若对话框尚未显示, 缓存最新 content, 等显示后回放. + pendingContentRef.set(contentList.toList()) + return + } + + val now = SystemClock.uptimeMillis() + + if (now - lastContentUpdateTime[0] < PROGRESS_UPDATE_MIN_INTERVAL_MS) { + return + } + + lastContentUpdateTime[0] = now + + GlobalAppContext.post { + try { + dialog.contentView?.setLinesEllipsizedIndividually(contentList.toList(), 1.5f) + } catch (_: Throwable) { + /* Ignored. */ + } + } + } + + fun setProgressThrottled(processed: Long, total: Long) { + val dialog = dialogRef.get() + if (dialog == null) { + // Cache the latest progress if dialog is not shown yet. + // zh-CN: 若对话框尚未显示, 缓存最新进度, 等显示后回放. + pendingProcessedRef.set(processed) + pendingTotalRef.set(total) + return + } + + val now = SystemClock.uptimeMillis() + + if (processed == lastProgressValue[0]) { + return + } + if (now - lastProgressUpdateTime[0] < PROGRESS_UPDATE_MIN_INTERVAL_MS) { + return + } + + lastProgressUpdateTime[0] = now + lastProgressValue[0] = processed + + GlobalAppContext.post(Runnable { + try { + dialog.setProgressNumberFormatByBytes(processed, total, true) + + var v: Int + val max: Int + + if (total > 0 && total <= Int.MAX_VALUE) { + max = total.toInt() + v = max(0L, min(processed, max.toLong())).toInt() + } else if (total > 0) { + max = PROGRESS_MAX_LARGE + val ratio = processed.toDouble() / total.toDouble() + v = (ratio * max.toDouble()).roundToInt() + v = max(0, min(max, v)) + } else { + // No total bytes, keep indeterminate behavior. + // zh-CN: 无总字节数, 保持不确定进度行为. + return@Runnable + } + + dialog.setMaxProgress(max) + dialog.setProgress(v) + } catch (_: Throwable) { + /* Ignored. */ + } + }) + } + + fun dismissSafely() { + // Mark closed first so delayed show won't create a dialog after completion. + // zh-CN: 先标记 closed, 防止延迟 show 在完成后创建对话框. + closed.set(true) + + val dialog = dialogRef.get() ?: return + + val now = SystemClock.uptimeMillis() + var delay = 0L + + if (shown) { + val shownDuration = now - shownAtUptimeMs + if (shownDuration < PROGRESS_DIALOG_MIN_SHOW_MS) { + delay = PROGRESS_DIALOG_MIN_SHOW_MS - shownDuration + } + } + + GlobalAppContext.postDelayed({ + try { + if (dialog.isShowing) { + dialog.dismiss() + } + } catch (_: Throwable) { + /* Ignored. */ + } + }, delay) + } + } + + // Controller for cancellable operations (copy/move/delete). + // zh-CN: 用于可取消操作 (复制/移动/删除) 的控制器. + class OperationController { + val cancelled: AtomicBoolean = AtomicBoolean(false) + + fun cancel() { + cancelled.set(true) + } + + fun throwIfCancelled() { + if (cancelled.get()) { + throw OperationAbortedException() + } + } + } + + class OperationAbortedException internal constructor() : RuntimeException("Operation aborted") + + private fun TextView.setUnknown() = setText(R.string.text_unknown) + private fun TextView.isUnknown() = text == context.getString(R.string.text_unknown) + private fun TextView.isEllipsisSix() = text == context.getString(R.string.ellipsis_six) + private fun TextView.isNullOrBlank() = text.isNullOrBlank() +} diff --git a/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt b/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt index 7f20969b..bcfdf757 100644 --- a/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/IntentUtils.kt @@ -38,7 +38,7 @@ import org.autojs.autojs.runtime.api.WrappedShizuku import org.autojs.autojs.ui.enhancedfloaty.FloatyService import org.autojs.autojs.ui.floating.FloatyWindowManger import org.autojs.autojs.util.ContextUtils.findActivity -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor import org.autojs.autojs.util.StringUtils.key import org.autojs.autojs6.R import java.io.File diff --git a/app/src/main/java/org/autojs/autojs/util/MaterialDialogUtils.kt b/app/src/main/java/org/autojs/autojs/util/MaterialDialogUtils.kt deleted file mode 100644 index 148e2b02..00000000 --- a/app/src/main/java/org/autojs/autojs/util/MaterialDialogUtils.kt +++ /dev/null @@ -1,99 +0,0 @@ -package org.autojs.autojs.util - -import android.view.View -import android.widget.TextView -import com.afollestad.materialdialogs.MaterialDialog -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.autojs.autojs.theme.ThemeColorHelper -import org.autojs.autojs6.R -import kotlin.text.isNullOrBlank - -object MaterialDialogUtils { - - fun MaterialDialog.makeSettingsLaunchable(viewGetter: (dialog: MaterialDialog) -> View?, packageName: String?) { - viewGetter(this)?.setOnClickListener { - packageName?.let { pkg -> - IntentUtils.launchAppDetailsSettings(this.context, pkg) - } ?: ViewUtils.showSnack(this.view, R.string.error_app_not_installed) - } - } - - @JvmStatic - fun MaterialDialog.makeTextCopyable(textViewGetter: (dialog: MaterialDialog) -> TextView?) { - makeTextCopyable(textViewGetter(this)) - } - - @JvmStatic - @JvmOverloads - fun MaterialDialog.makeTextCopyable(textView: TextView?, textValue: String? = textView?.text?.toString()) { - if (!textValue.isNullOrBlank()) { - val context = this.context - textView?.setOnClickListener { - ClipboardUtils.setClip(context, textValue) - ViewUtils.showSnack(this.view, "${context.getString(R.string.text_already_copied_to_clip)}: $textValue") - } - } - } - - fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, textValuePair: Pair) { - setCopyableTextIfAbsent(textView, textValuePair.first, textValuePair.second) - } - - fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, textValue: String?, suffix: String? = null) { - when { - textValue.isNullOrBlank() -> { - if (textView.isNullOrBlank() || textView.isEllipsisSix()) { - textView.setUnknown() - } - } - textView.isNullOrBlank() || textView.isEllipsisSix() || textView.isUnknown() -> { - textView.text = textValue + (suffix ?: "") - this.makeTextCopyable(textView, textValue) - } - } - } - - fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, scope: CoroutineScope, f: () -> String?) { - setCopyableTextIfAbsent(textView, scope, f, suffix = null) - } - - fun MaterialDialog.setCopyableTextIfAbsent(textView: TextView, scope: CoroutineScope, f: () -> String?, suffix: String?) { - scope.launch(Dispatchers.IO) { - val textValue = f.invoke() - withContext(Dispatchers.Main) { - setCopyableTextIfAbsent(textView, textValue, suffix) - } - } - } - - fun MaterialDialog.setCopyableText(textView: TextView, textValue: String?) { - when { - textValue.isNullOrBlank() -> { - textView.setUnknown() - } - else -> { - textView.text = textValue - this.makeTextCopyable(textView, textValue) - } - } - } - - @JvmStatic - fun MaterialDialog.Builder.choiceWidgetThemeColor() = also { - it.choiceWidgetColor(ThemeColorHelper.getThemeColorStateList(context)) - } - - @JvmStatic - fun MaterialDialog.Builder.widgetThemeColor() = also { - it.widgetColor(ThemeColorHelper.getThemeColorStateList(context)) - } - - private fun TextView.setUnknown() = setText(R.string.text_unknown) - private fun TextView.isUnknown() = text == context.getString(R.string.text_unknown) - private fun TextView.isEllipsisSix() = text == context.getString(R.string.ellipsis_six) - private fun TextView.isNullOrBlank() = text.isNullOrBlank() - -} \ No newline at end of file diff --git a/app/src/main/java/org/autojs/autojs/util/StringUtils.kt b/app/src/main/java/org/autojs/autojs/util/StringUtils.kt index d51cf414..81b978dc 100644 --- a/app/src/main/java/org/autojs/autojs/util/StringUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/StringUtils.kt @@ -20,7 +20,7 @@ import kotlin.math.pow /** * Created by Stardust on May 3, 2017. - * Modified by SuperMonster003 as of Jul 6, 2022. + * Modified by SuperMonster003 as of Feb 8, 2026. */ object StringUtils { @@ -364,6 +364,14 @@ object StringUtils { fun String.isLooselyIn(list: Iterable): Boolean = list.any { this.equalsLoosely(it) } + @JvmStatic + fun String.normalizeTrailingSlash(isDir: Boolean): String { + if (this == "/") return "/" + + val noTail = this.trimEnd('/') + return if (isDir) "$noTail/" else noTail + } + class CharsetMatchWrapper(private val charsetMatch: CharsetMatch?) { val name: String? by lazy { charsetMatch?.name } diff --git a/app/src/main/java/org/autojs/autojs/util/UpdateUtils.kt b/app/src/main/java/org/autojs/autojs/util/UpdateUtils.kt index 600a48ed..8a233cb7 100644 --- a/app/src/main/java/org/autojs/autojs/util/UpdateUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/UpdateUtils.kt @@ -4,8 +4,7 @@ import android.content.Context import androidx.appcompat.app.AppCompatActivity import com.afollestad.materialdialogs.DialogAction import com.afollestad.materialdialogs.MaterialDialog -import org.autojs.autojs.app.DialogUtils -import org.autojs.autojs.app.DialogUtils.showAdaptive +import org.autojs.autojs.util.DialogUtils.showAdaptive import org.autojs.autojs.core.pref.Pref import org.autojs.autojs.core.pref.Pref.isAutoCheckForUpdatesEnabled import org.autojs.autojs.core.pref.Pref.lastNoNewerUpdatesTimestamp @@ -14,7 +13,7 @@ import org.autojs.autojs.core.pref.Pref.lastUpdatesPostponedTimestamp import org.autojs.autojs.network.UpdateChecker import org.autojs.autojs.network.UpdateChecker.PromptMode import org.autojs.autojs.network.entity.VersionInfo -import org.autojs.autojs.util.MaterialDialogUtils.choiceWidgetThemeColor +import org.autojs.autojs.util.DialogUtils.choiceWidgetThemeColor import org.autojs.autojs6.R /** diff --git a/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt b/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt index 6eefa19f..8b90de29 100644 --- a/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/ViewUtils.kt @@ -38,7 +38,6 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewTreeObserver import android.view.Window -import android.view.WindowInsets import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.EditText @@ -64,6 +63,7 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.children import androidx.core.view.get import androidx.core.view.size +import androidx.core.view.updateLayoutParams import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import org.autojs.autojs.app.GlobalAppContext @@ -80,11 +80,13 @@ import android.text.TextUtils as AndroidTextUtils /** * Created by Stardust on Jan 24, 2017. * Modified by SuperMonster003 as of Feb 2, 2026. + * Modified by OpenAI ChatGPT (GPT-5.2 Thinking) as of Feb 7, 2026. */ @Suppress("unused") object ViewUtils { - private const val TAG_STATUS_BAR_SCRIM = "status_bar_scrim" + private const val STATUS_BAR_SCRIM_TAG = "autojs6:status_bar_scrim" + private const val NAV_BAR_SCRIM_TAG = "autojs6:nav_bar_scrim" @JvmStatic var isKeepScreenOnWhenInForegroundEnabled @@ -747,9 +749,9 @@ object ViewUtils { private fun installOrUpdateScrim(activity: Activity, @ColorInt color: Int) { val decor = activity.window.decorView as ViewGroup - val scrim = decor.children.find { it.tag == TAG_STATUS_BAR_SCRIM } + val scrim = decor.children.find { it.tag == STATUS_BAR_SCRIM_TAG } ?: FrameLayout(activity).also { - it.tag = TAG_STATUS_BAR_SCRIM + it.tag = STATUS_BAR_SCRIM_TAG // Height will be set by Insets later. // zh-CN: 高度稍后由 Insets 赋值. val lp = FrameLayout.LayoutParams(MATCH_PARENT, 0, Gravity.TOP) @@ -789,7 +791,6 @@ object ViewUtils { fun setNavigationBarBackgroundColor(activity: Activity, color: Int) { val window = activity.window - val decorView = window.decorView if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { @Suppress("DEPRECATION") @@ -797,18 +798,66 @@ object ViewUtils { return } - decorView.setOnApplyWindowInsetsListener { view, insets -> - val navBarInsets = insets.getInsets(WindowInsets.Type.navigationBars()) - val contentView: ViewGroup? = activity.findViewById(android.R.id.content) - val navBarOverlay = View(activity).apply { + val content = activity.findViewById(android.R.id.content) ?: return + + val scrim = (content.findViewWithTag(NAV_BAR_SCRIM_TAG) ?: run { + View(activity).apply { + tag = NAV_BAR_SCRIM_TAG + // Avoid intercepting touch/accessibility focus. + // zh-CN: 避免拦截触摸/无障碍焦点. + isClickable = false + isFocusable = false + importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + layoutParams = FrameLayout.LayoutParams( MATCH_PARENT, - navBarInsets.bottom, // Navigation bar height. (zh-CN: 导航栏高度.) - ).apply { gravity = Gravity.BOTTOM } - setBackgroundColor(color) + 0, + ).apply { + gravity = Gravity.BOTTOM + } + + // Key: listener is attached to scrim itself, not touching decorView. + // zh-CN: 关键: listener 挂在 scrim 自己身上, 不去动 decorView. + ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets -> + val nav = insets.getInsets(WindowInsetsCompat.Type.navigationBars()) + val tap = insets.getInsets(WindowInsetsCompat.Type.tappableElement()) + val bottom = maxOf(nav.bottom, tap.bottom) + + v.updateLayoutParams { + height = bottom + gravity = Gravity.BOTTOM + } + insets + } + }.also { created -> + content.addView(created) + created.requestApplyInsetsWhenAttached() } - contentView?.addView(navBarOverlay) - view.onApplyWindowInsets(insets) + }) + + // Update color without depending on insets callback (avoid "probabilistic ineffectiveness"). + // zh-CN: 更新颜色不依赖 insets 回调 (避免 "概率不生效"). + scrim.setBackgroundColor(color) + scrim.bringToFront() + } + + // If requestApplyInsets() is called when view is not attached, it will be discarded. + // Need to wait until attached and request again to reliably trigger insets dispatch. + // zh-CN: + // 如果 view 未 attach 时调用 requestApplyInsets() 会被丢弃. + // 需要等 attach 后再请求一次, 才能稳定触发 insets 分发. + private fun View.requestApplyInsetsWhenAttached() { + if (ViewCompat.isAttachedToWindow(this)) { + ViewCompat.requestApplyInsets(this) + } else { + addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + v.removeOnAttachStateChangeListener(this) + ViewCompat.requestApplyInsets(v) + } + + override fun onViewDetachedFromWindow(v: View) = Unit + }) } } @@ -1075,7 +1124,7 @@ object ViewUtils { val desaturate = ColorMatrix().apply { setSaturation(0f) } // Construct the alpha scaling matrix. // zh-CN: 构造透明度缩放矩阵. - val alphaMatrix = ColorMatrix().apply { setScale(1f, 1f, 1f, 0.5f) } + val alphaMatrix = ColorMatrix().apply { setScale(1f, 1f, 1f, alpha) } // Concatenate: first desaturate, then apply alpha. // zh-CN: 叠加: 先灰度, 再透明度. desaturate.postConcat(alphaMatrix) @@ -1149,18 +1198,17 @@ object ViewUtils { fun showToast(context: Context, stringRes: Int, isLong: Boolean) = showToast(context, context.getString(stringRes), isLong) @JvmStatic - fun showToast(context: Context, string: String?) = showToast(context, string, false) + fun showToast(context: Context, string: CharSequence?) = showToast(context, string, false) @JvmStatic - fun showToast(context: Context, string: String?, isLong: Boolean) { - string?.let { - when { - Looper.getMainLooper() == Looper.myLooper() -> { - Toast.makeText(context, it, if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() - } - else -> GlobalAppContext.post { - Toast.makeText(context, it, if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() - } + fun showToast(context: Context, string: CharSequence?, isLong: Boolean) { + string ?: return + when { + Looper.getMainLooper() == Looper.myLooper() -> { + Toast.makeText(context, string, if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() + } + else -> GlobalAppContext.post { + Toast.makeText(context, string, if (isLong) Toast.LENGTH_LONG else Toast.LENGTH_SHORT).show() } } } @@ -1178,10 +1226,11 @@ object ViewUtils { } @JvmStatic - fun showSnack(view: View, string: CharSequence) = showSnack(view, string, false) + fun showSnack(view: View, string: CharSequence?) = showSnack(view, string, false) @JvmStatic - fun showSnack(view: View, string: CharSequence, isLong: Boolean) { + fun showSnack(view: View, string: CharSequence?, isLong: Boolean) { + string ?: return try { Snackbar.make(view, string, if (isLong) Snackbar.LENGTH_LONG else Snackbar.LENGTH_SHORT).show() } catch (e: IllegalStateException) { @@ -1199,7 +1248,8 @@ object ViewUtils { } @JvmStatic - fun showSnack(view: View, string: CharSequence, duration: Int) { + fun showSnack(view: View, string: CharSequence?, duration: Int) { + string ?: return try { Snackbar.make(view, string, duration).show() } catch (e: IllegalStateException) { @@ -1329,7 +1379,7 @@ object ViewUtils { fun TextView.setLinesEllipsizedIndividually( lines: List, lineSpacing: Float = 0f, - where: AndroidTextUtils.TruncateAt = AndroidTextUtils.TruncateAt.MIDDLE + where: AndroidTextUtils.TruncateAt = AndroidTextUtils.TruncateAt.MIDDLE, ) { post { val avail = width - paddingLeft - paddingRight diff --git a/app/src/main/java/org/autojs/autojs/util/WorkingDirectoryUtils.kt b/app/src/main/java/org/autojs/autojs/util/WorkingDirectoryUtils.kt index 36d426ed..b2267ed3 100644 --- a/app/src/main/java/org/autojs/autojs/util/WorkingDirectoryUtils.kt +++ b/app/src/main/java/org/autojs/autojs/util/WorkingDirectoryUtils.kt @@ -12,9 +12,9 @@ import java.io.File object WorkingDirectoryUtils { - var histories: LinkedHashSet - get() = Pref.getLinkedHashSet(R.string.key_working_directory_histories) - private set(value) = Pref.putLinkedHashSet(R.string.key_working_directory_histories, value) + var history: LinkedHashSet + get() = Pref.getLinkedHashSet(R.string.key_working_directory_history) + private set(value) = Pref.putLinkedHashSet(R.string.key_working_directory_history, value) @JvmStatic var path: String @@ -86,12 +86,12 @@ object WorkingDirectoryUtils { } @JvmStatic - fun addIntoHistories(history: CharSequence) { - histories = linkedSetOf(toRelativePath(history.toString())).apply { addAll(histories) } + fun addIntoHistory(item: CharSequence) { + history = linkedSetOf(toRelativePath(item.toString())).apply { addAll(history) } } - fun removeFromHistories(history: CharSequence) { - histories = histories.apply { remove(toRelativePath(history.toString())) } + fun removeFromHistory(item: CharSequence) { + history = history.apply { remove(toRelativePath(item.toString())) } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about_version_histories.png b/app/src/main/res/drawable/ic_about_release_history.png similarity index 100% rename from app/src/main/res/drawable/ic_about_version_histories.png rename to app/src/main/res/drawable/ic_about_release_history.png diff --git a/app/src/main/res/drawable/ic_search_smaller_thicker_black_48dp.png b/app/src/main/res/drawable/ic_search_smaller_thicker_black_48dp.png new file mode 100644 index 0000000000000000000000000000000000000000..34d9e67e564f76be28e7d1743486f12ee2d1930f GIT binary patch literal 3850 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_simple_file.png b/app/src/main/res/drawable/ic_simple_file.png new file mode 100644 index 0000000000000000000000000000000000000000..584ba271c93b68165e45686cfde9ccbc0ef02d94 GIT binary patch literal 913 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7uRSoCO|{#S9G08$g&*{RsbBprB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&K-F@dE{-7;bKc&z&k_k_IQ}s| z$wAm!+WM$h_TC1KC`pn2hpc-VwqM}Zh?4m4(ABZzQnyD)PRoRji4i)t-?kY3PP->v ze_wx3n%KQJdw!;*Z9e04^Br@3Va4hgUt03U8cb0=f!>&+wg@#|DKs9Eo+8h|(hOjm={M);3 zd7YlfjB4qMibHl?_K{n{vKB4aSCUhI!zp>NhpvT|9SK^cbJB-#Y%7_4dCD2kMJXhBs`C zN?f4b5YN)8-td7%U>?IEVTTGP4u6J+oC-FKEyF@koAJSp+p%%y+b8?K2u$OW%XZRc4F{! L^>bP0l+XkK8!B`C literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_simple_folder.png b/app/src/main/res/drawable/ic_simple_folder.png new file mode 100644 index 0000000000000000000000000000000000000000..1c44ddbabceb2078b85ecc2f684cc0a050606207 GIT binary patch literal 2075 zcmZ8ieK^zW8~^Ub+q|X5#-vdcWwr8FDOz@HhZw0}N^QtnMPk(ww&8du^fDbULr79y za}?RPR4P(lI=D`4`^ANXNomez+wZIE{Qf!5b>Gi@-_LV>KKJLjp69xr44Rj_F3JQ2 z0Dvx);^qrQ&WdZQLvz>Oz#b?dV@Xs$O=!exo=k^ot!RpWEC3)HR~)vOJWGWZjhW;l z%!6k`nHK|NLV$}GFA^f6PR9lZMu!m2#)K8kIhz20Mj_R0pC7Auw!}XhO~!V!+qXEz z)a|1nGqO--HF;(G)H2{GR$h8{vIf!IN*4P4iH5yNPRdfyr>n%h;XxMI{S_Z9O24`~ zJK*|PrEj7bJww1_W^?tQM~#%$p9*cgdp9nwlkPiuX4dnK<9n6jQTw#$P}!!;ihmjU zMm37f*ZB}PG3mr$Oc=Z>29)p%03We7|5G?N3f2m%D!x$yj)4tzyUKiw_3A8?u=}ku zYEf@1H=5-2gBL!n8&Y1o?zgwZT2CForL*ya*M?~ZL!Lc1`Bwt_q)-D-38RM-o ze83Hi%rW~Ou84z4a`VIg0@uh>-E|PpeSg;i+Y0N>ADe~GWg8tYS-{7Hbtdn$AJ4x7 z62{x~5xj2gdrB5>!OJ=QmCmJ5iw2xOayR!+R{493fF|VDyXDlx-ACr)vl#3De#3Z3 zng&CNh}wKhSH~vJ6S>&`e8ivs@X=>|ZOLS#$c&JMIh;U4!t4;I^8`H+VjpInNfcZHqA?ue1#mHU6Mzfc)uFO1GT$`qo+p-UpUWHpSVy)ic zeN~IR8UVM5>7Uj)i$c~l3&bE${pMAGnPG8!o4mJK(GEdGqW^fcJT z@G!6^fU`3#>Un@)30?{FJm!UPT$(Pc&@MAGMh3v0b(fRlvpb&--n)mwi#%WKog_L& zj=hP%(YA+SqA{VhyI&QwvfLR`%jugGOChUG02lSE2ia zL-3Z^kn!`|r)M|T{wqo)A_b7 z1?{mr^eZZ%>9#5$?ToD)TfRLN5VkyPRL8D%x>3=Tw4j`ItI@ILDb{az*t8c_>mx;Cb*bPqWo+l&c0^8aB29m0%}Wr*nTP5V;VW z@&!Lfn`bdnY4x8)TeUe)NQ$HBn>pl{w=TymN-)+v%A8AWc8`F9<=iOs|6a8kZ?DTK zbnm^T#lUa#uHluk56a#$#qs5xQahXn&`gX~QSB6R^6RFLV09@yNXr#9<23IBNy%jv zDRjeZ_h38#U&b+Ra5=(y+r2_=>xP86rXWo*sW(Qlf($WO2pUb0?MwxtnuqH3O~_yK zn`RPwgVsELwu-}4in#tL^4Bha(Mc2NjUlEiTYyx!BWiJ%Y?9{>J|rcKMmV$p6Xxyl z-`6s~cD+LO zPYJ|@4MnUHLtnu6s7;`i6`D;^bl{qI9x*mo!yhj^qoLHq?(a9wUS?iDUtoI;c)^G| zP!KMY_5@@jHf22E@7H3$7J|?=fPnH@wP`+MR<|69uTmw0t*(60CJKBc>1KqXmN_SM zyyVqz@I?J5N$6+yBSs^ zb-Aq0s2;s~?UL&FNpqk`N*@%^Ykw%)U;b2|tNa_f`U;5AXoV45*}kP80&2r3Le(2c z(nfm-w`9b5BVT!K{}hlu+EuCOx3e(HWR-*_5qO`12yk5v=dy;=BZ~q?bf8g&E+b`dVY_7H> z;Vi{MJFay|o!i`GMubTMI54&2P$wH3T5^wl7vCeA_yVuL=CR`RchQ(= z_HR->H6ccL3@-c?S2)n)CJq}O2c)L&>j~ygxd=ug1JrU=4*H5-S#M3_q|K!DtvdiFfch{& zv5}NZ=XUgwY-*|NBMZ+dsfF%&Oh?I#b>RJFDS{t3x9IJbL-9eA|7CYvlp2AZUWSC% SO_!mM9H5fD-0qVYN&f?oQQ0v7 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/ic_version_history.png b/app/src/main/res/drawable/ic_version_history.png index ea8f810ffd70c61e9f6d06393b4796458d8a3ebe..15e8fe4236cbaf4f95028866b3302c432c19cd85 100644 GIT binary patch delta 15695 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 SK6WI-S3oefz?U8;Ui&}9-tMvh delta 18547 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 diff --git a/app/src/main/res/layout/activity_about_function_buttons.xml b/app/src/main/res/layout/activity_about_function_buttons.xml index 12e532d4..41d4bfb7 100644 --- a/app/src/main/res/layout/activity_about_function_buttons.xml +++ b/app/src/main/res/layout/activity_about_function_buttons.xml @@ -123,7 +123,7 @@ + app:layout_constraintBottom_toTopOf="@id/guideline_50_release_history" /> + android:theme="@style/ToolBarStyle" + app:popupTheme="@style/AppTheme.PopupOverlay" /> diff --git a/app/src/main/res/layout/activity_display_version_histories.xml b/app/src/main/res/layout/activity_display_release_history.xml similarity index 100% rename from app/src/main/res/layout/activity_display_version_histories.xml rename to app/src/main/res/layout/activity_display_release_history.xml diff --git a/app/src/main/res/layout/activity_project_config.xml b/app/src/main/res/layout/activity_project_config.xml index a8e4a0b8..e3903ad6 100644 --- a/app/src/main/res/layout/activity_project_config.xml +++ b/app/src/main/res/layout/activity_project_config.xml @@ -16,7 +16,8 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:theme="@style/ToolBarStyle" /> + android:theme="@style/ToolBarStyle" + app:popupTheme="@style/AppTheme.PopupOverlay"/> diff --git a/app/src/main/res/layout/activity_trash.xml b/app/src/main/res/layout/activity_trash.xml new file mode 100644 index 00000000..11daf44f --- /dev/null +++ b/app/src/main/res/layout/activity_trash.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_version_history.xml b/app/src/main/res/layout/activity_version_history.xml new file mode 100644 index 00000000..11daf44f --- /dev/null +++ b/app/src/main/res/layout/activity_version_history.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_version_history.xml b/app/src/main/res/layout/item_release_history.xml similarity index 100% rename from app/src/main/res/layout/item_version_history.xml rename to app/src/main/res/layout/item_release_history.xml diff --git a/app/src/main/res/layout/item_trash.xml b/app/src/main/res/layout/item_trash.xml new file mode 100644 index 00000000..e80d8487 --- /dev/null +++ b/app/src/main/res/layout/item_trash.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_color_items.xml b/app/src/main/res/menu/menu_color_items.xml index bf9e1d63..118d8b9f 100644 --- a/app/src/main/res/menu/menu_color_items.xml +++ b/app/src/main/res/menu/menu_color_items.xml @@ -6,7 +6,7 @@

@@ -83,6 +84,7 @@ @@ -145,7 +147,9 @@ android:title="@string/text_pinch_to_zoom" app:showAsAction="never" /> - + + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools"> diff --git a/app/src/main/res/menu/menu_trash.xml b/app/src/main/res/menu/menu_trash.xml new file mode 100644 index 00000000..6b76ee08 --- /dev/null +++ b/app/src/main/res/menu/menu_trash.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_version_history.xml b/app/src/main/res/menu/menu_version_history.xml new file mode 100644 index 00000000..1e319755 --- /dev/null +++ b/app/src/main/res/menu/menu_version_history.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6b7daa21..cf4ccdf5 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -2,6 +2,8 @@ + + @@ -99,6 +101,7 @@ \"موقع المؤشر\" هي ميزة تصحيح أخطاء ضمن خيارات المطوّر في Android.\nعند تفعيلها، يعرض النظام على الشاشة معلومات نقاط اللمس مثل [الإحداثيات/مسار الحركة/العدد/الحجم/سرعة الحركة/الضغط]، مما يسهل [كتابة/تصحيح/التحقق من] السكربتات ذات الصلة. يسمح إذن \"إرسال الإشعارات\" لـ AutoJs6 بنشر الإشعارات على النظام، مما يتيح للسكربتات نشر وإدارة إشعارات مخصّصة في شريط الإشعارات.\n\nNote: على أجهزة Android 13+، إذا لم يتم منح هذا الإذن فقد لا تظهر بعض الإشعارات، وقد يؤثر ذلك على تشغيل خدمات المقدمة واستقرارها. مع الوصول إلى وسائط المشروع ، لن يطلب منك التحذير الأمني لتسجيل الشاشة. + عرض سجل إصدارات AutoJs6 على GitHub وإحصاءات الفئات الرئيسية. تؤثر استراتيجية إعادة التشغيل فقط في زر إعادة التشغيل في درج الصفحة الرئيسية.\n\nإعادة تشغيل سريعة: تعيد تشغيل التطبيق بسرعة. إذا فشلت عملية إعادة التشغيل أو حدثت حالات غير متوقعة، جرّب التبديل إلى \"إعادة تشغيل مجدولة\".\nإعادة تشغيل مجدولة: تُعدّ مسبقًا مهمة مؤقتة قصيرة. بعد إيقاف التطبيق، سيبدأ مرة أخرى وفق الجدول لتنفيذ إعادة التشغيل. عند تفعيل المفتاح (الوضع الافتراضي): سيتم تغليف القيم المُعادة من أساليب Java من الأنواع Number/Boolean/Character ككائنات Java وإتاحتها للسكريبت (باستثناء String). ستكون typeof هي \"object\" و species هي \"JavaObject\"، ويمكن استدعاء أساليب Java، مما يساعد على الحفاظ على خصائص الأنواع الدقيقة في Java وحل زيادة التحميل (overload).\n\nعند إيقاف المفتاح: لن تُغلف الأنواع المذكورة، وستُعرض مباشرة كقيم أولية في JavaScript (number/boolean/سلسلة ذات حرف واحد). ستكون typeof هي نوع JavaScript الموافق، وهو أقرب لدلالات ونظام JavaScript البيئي. لا يزال بالإمكان إعلان مغلّف Java صراحةً باستخدام new، مثل new java.lang.Boolean(true).\n\nانظر: http://issues.autojs6.com/435 إذا كان لديك جذر غريب أو حالة غير طبيعية للوصول إلى الجذر ، فيمكنك إجبار الجذر على الجذر أو غير الجذر. @@ -111,7 +114,6 @@ يتحكم في الآلية الأساسية لتشغيل السكربتات بشكل مجدول.\n\nAlarmManager: عبر منح إذن \"السماح بتعيين المنبّهات والتذكيرات\" يمكن أن تصبح المهام أقرب إلى التنفيذ في الوقت المحدد.\nWorkManager: صديق للنظام؛ مناسب للمهام غير الحرجة التي تتحمل بعض التأخير.\nJobScheduler: تنفيذ قديم محفوظ للتوافق؛ احتمال التأخير أعلى. حالات الاستخدام والفروقات:\n\n1. AlarmManager\nمناسب للمهام الحسّاسة للتوقيت، مثل [التذكيرات/السكربتات الدقيقة التوقيت/...].\nعلى Android 12+ ومع منح إذن \"السماح بتعيين المنبّهات والتذكيرات\" يمكن التنفيذ بدقة أكبر حتى مع إيقاف الشاشة أو في وضع السكون.\nبدون الإذن قد يقلل النظام الدقة أو يؤخر التنفيذ.\n\n2. WorkManager\nمناسب للمهام التي لا تتطلب دقة زمنية صارمة مثل [مزامنة غير حرجة/تنظيف/إحصاءات/...].\nيتم الجدولة وفق [حالة البطارية/الشبكة/الشحن/سياسات السكون/...], وقد تتأخر المهام عند إيقاف الشاشة أو في وضع السكون.\nلا يضمن الدقة الزمنية لكنه يوفر مزايا في [إتمام موثوق/إعادة المحاولة عند الفشل/سلاسل المهام/إزالة التكرار].\n\n3. JobScheduler\nتنفيذ تاريخي لأغراض التوافق.\nفي إصدارات أندرويد الحديثة قد تحدث تأخيرات أكبر.\nلا يُنصح به إلا لاعتبارات التوافق. يوفر إمكانية الوصول إلى سجل وإحصائيات استخدام الجهاز مما يؤدي إلى CurrentPackage () نتيجة أكثر دقة - عرض سجل إصدارات AutoJs6 على GitHub وإحصاءات الفئات الرئيسية. إعدادات النظام الآمنة ، التي تحتوي على تفضيلات النظام التي يمكن أن تقرأها التطبيقات ولكن لا يُسمح لها بالكتابة.\nهذه هي لتفضيلات يجب على المستخدم تعديلها بشكل صريح من خلال واجهة المستخدم لتطبيق النظام.\nمع إذن إعدادات النظام الآمن ، يمكن للتطبيقات العادية تعديل الإعدادات الآمنة مباشرة (مثل خدمة إمكانية الوصول). يسمح إذن \"تعديل إعدادات النظام\" لـ AutoJs6 بتعديل بعض إعدادات النظام، مما يتيح للسكربتات تعديل إعدادات مثل [ سطوع الشاشة / التدوير التلقائي / مهلة إيقاف الشاشة ]. تخلَّ @@ -122,11 +124,13 @@ @string/text_back يلغي يلغي + اختيار مسح نعم اتصال @string/text_continue @string/text_copy + نسخ بادئة افتراضية تفاصيل رفض @@ -141,17 +145,20 @@ تصغير أكثر فتح لوحة الألوان + معاينة يترك + السجل إزالة + @string/text_restore جلب إعادة المحاولة يحفظ + حفظ باسم اعدادات النظام النظام تحديث استخدم الافتراضي ستخدام اللوحة - السجل استعراض الملف عرض الاختصارات عرض التحديث @@ -214,7 +221,6 @@ لا يمكن تحويل %s إلى مسار وقت تشغيل البرنامج النصي لا يمكن العثور على EditorView من عرض الطفل: %s لا يمكن الحصول على تنزيل عنوان URL - فشل في إعادة تسمية لا يمكن استرداد حالة البطارية استخدم المزيد من الأحرف لكلمة المرور لا تتطابق كلمات المرور هذه @@ -242,6 +248,8 @@ تعذّر تغيير حالة المفتاح فشل تحويل القيمة %s إلى قابل للرسم + فشل النسخ + فشل في انشاء فشل فتح صفحة الإعدادات فشل منح Shizuku حق الوصول فشل إنشاء المثيل \"%s\" @@ -249,10 +257,12 @@ تعذّر تشغيل المدير تعذّر فتح إعدادات النظام فشل تحميل المكونات الإضافية.\nالسبب: %1$s. + فشل النقل + فشل في إعادة تسمية فشل في عرض المحتوى تعذّر جلب سجل التغييرات + فشل في جلب سجل الاصدارات تعذّر جلب ملاحظات الإصدار - تعذّر جلب سجل الإصدارات تعذّر جلب ملف version.properties لتحليل معلومات الإصدار فشل في إلغاء وصول Shizuku فشل في تشغيل البرنامج النصي @@ -276,6 +286,7 @@ مستودع جيثب غير صالح: %s عنوان إب غير صالح اعتماد تسجيل الدخول غير صالح + مسار غير صالح: %1$s بوصلة راحة غير صالحة: %s عنصر خريطة محدد غير صالح: {%s: %s (%s)} لا يمكن أن يكون عنوان IP فارغًا @@ -320,11 +331,13 @@ رقم المنفذ أكثر من 65535 ملف البرنامج النصي الرئيسي للمشروع \"%1$s\" غير موجود لا يمكن وضع القيمة %s في JSON + فشل التحديث لا يمكن العثور على السابق مع Regex رمز القولون المتكرر تكرار رمز النقطة الخاصية المطلوبة \"%1$s\" خالية أو غير موجودة المسار المُحلّ لـ \"%1$s\" لا يمكن أن يكون null + فشل الحفظ السيناريو عند الخروج يجب أن تسمى طريقة المحدد بدلاً من تمريرها كمعلمة: %s لا يمكن استخدام بوصلة عندما يكون نوع النتيجة محددًا @@ -336,12 +349,18 @@ لا يمكن لمحول الخاصية المطلوبة \"%1$s\" إرجاع قيمة فارغة الموضوع ليس على قيد الحياة انتهت مهلة الاستعلام عن معلومات المكوّن الإضافي %1$s. + مسار الملف الحالي غير معروف، لا يمكن عرض تفاصيل الملف + مسار الملف الحالي غير معروف، لا يمكن عرض سجل الاصدارات + مسار الملف الحالي غير معروف، لا يمكن الفتح بتطبيقات اخرى + مسار الملف الحالي غير معروف، لا يمكن حزم التطبيق + مسار الملف الحالي غير معروف، لا يمكن حفظ الملف غير قادر على استخدام خدمة Shizuku الطابع غير المقبول خطأ غير معروف محدد خوارزمية غير معروف بارام: %s اسم لون غير معروف: %s اسم عنصر غير معروف: %1$s + استثناء غير معروف الجاذبية غير معروفة: %s عملية غير معروفة نوع نتيجة غير معروف {name: %s} @@ -349,7 +368,8 @@ محدد غير معروف {الاسم: %s ، اكتب: %s} نوع غير معروف: %s لا يمكن ضغط تنسيق WebP-Lossless باستخدام معامل الجودة؛ استخدم JPEG/PNG/WebP-Lossy بدلاً من ذلك - الملف غير موجود أو قابلاً للقراءة: %s + الملف غير موجود او غير قابل للقراءة + الملف غير موجود أو قابلاً للقراءة: %s خدمة مقدمة %1$s انقر لإطلاق %1$s خدمة مقدمة %1$s @@ -364,6 +384,8 @@ 0 للحلقة اللانهائية يتم دعم IPv4 و IPv6 وأسماء النطاقات. أدخل عنوان URL يشير إلى ملحق (Plugin) بعيد.\nمثال: \"https://example.com/plugin.apk\". + حذف نهائيا + نقل الى سلة المحذوفات آخر استخدام: %1$s فشل خيط \"blob\" نجح خيط \"blob\" وجرى حفظ التخزين المؤقت دون اتصال @@ -431,12 +453,12 @@ السماح للتطبيق بإنشاء وإرسال الإشعارات تنفيذ قبل ملف البرنامج النصي إظهار الرسالة عند بدء أو إيقاف تسجيل البرنامج النصي + عرض سجل الاصدارات والاحصاءات ستُغلَّف الأنواع البدائية في Java ككائنات Java داخل السكربت تحليل تخطيط أكثر استقرارًا ولكن توافق رمز أسوأ (إعادة تشغيل خدمة إمكانية الوصول المطلوبة) أضف اختصارات إلى المشغل ابدأ أو توقف عن التسجيل الذي يتم التحكم فيه بواسطة مفتاح خفض مستوى الصوت عند عرض الزر العائم اضغط على مفتاح \"رفع الصوت\" لإيقاف تشغيل جميع البرامج النصية - عرض سجل إصدارات النُّسَخ والبيانات الإحصائية وحدة التخزين الداخلية خدمة إمكانية الوصول مطلوب من خلال العملية التلقائية البرنامج النصي (انقر فوق ، اضغط لفترة طويلة ، شريحة ، إلخ). @@ -450,8 +472,8 @@ حول العرض على التطبيقات الأخرى استجابة التراخيص + @string/text_release_history تحديث - @string/text_version_histories وقد تم منح الوصول صوت نقر) انقر طويل (Longclick) @@ -468,8 +490,7 @@ الكل جميع الملفات وصول هناك حاجة إلى \"All Files Access\" للوصول إلى ملفات البرنامج النصي على الهاتف - جميع السجلات - تم مسح جميع السجلات + كل ادخالات السجل تم مسح جميع العناصر يُوصى بمنح إذن \"السماح بتعيين المنبّهات والتذكيرات\" لضمان تنفيذ المهام المجدولة بأكبر قدر ممكن من الدقة حتى عند إيقاف الشاشة أو في وضع السكون نسخ إلى الحافظة @@ -510,8 +531,8 @@ توثيق AutoJs6 سجل سجل - AutoJs6 Plugin Center - Plugins + مركز اضافات AutoJs6 + الاضافات AutoJs6 إعدادات إعدادات مصدر الرمز @@ -523,6 +544,7 @@ تحقق من التحديثات التلقائية تحقق من وجود التحديثات تلقائيًا وعرض تناول وجبة خفيفة على الصفحة الرئيسية وضع الليل التلقائي + اعادة تسمية تلقائيا النوافذ المتاحة خلف عمل @@ -555,17 +577,23 @@ التحقق من وجود تحديثات (تجاهل النسخة المحلية) الحصول مباشرة على آخر نسخة عن بُعد عند التحقق من التحديثات البحث عن تحديثات... + اختر طريقة الحذف + اختر مسار الاستعادة المدينة أو المنطقة اسم الفئة / الحزمة صافي مسح اختيار الملف مسح السيناريو قبل التنفيذ + تفريغ سلة المحذوفات + هل تريد تفريغ جميع العناصر في سلة المحذوفات? تحديثات مسح الحالات التي تم فحصها - Click icon to add launcher shortcut + مسح سجل الاصدارات + هل تريد مسح جميع عناصر سجل الاصدارات? + اضغط على الايقونة لاضافة اختصار للشاشة الرئيسية انقر فوق العنصر لإزالته اضغط على العنصر لعرض التفاصيل انقر فوق \"نعم\" للانتقال إلى الإعدادات - Click other areas to exit selection + اضغط في مكان اخر للخروج من التحديد التشغيل المتكرر وضع العميل استنساخ مكتبة ألوان @@ -581,10 +609,10 @@ التوافق ثقة تكوين - هل أنت متأكد من مسح جميع السجلات؟ هل أنت متأكد من مسح جميع العناصر؟ - سيتم مسح السجلات التالية\n\n[ %1$s ]\n\nهل أنت متأكد من المسح؟ - سيتم مسح السجلات التالية\n\n[ جميع مكتبات الألوان ]\n\nهل أنت متأكد من المسح؟ + هل تريد مسح السجل? + سيتم مسح السجل ادناه\n\n[ %1$s ]\n\nهل تريد المتابعة? + سيتم مسح جميع ادخالات السجل ادناه\n\n[ جميع مكتبات الالوان ]\n\nهل تريد مسحها كلها? هل انت متأكد من الحذف؟ هل أنت متأكد من إزالته؟ هل تريد بالتأكيد إلغاء التثبيت؟ @@ -609,6 +637,7 @@ نسخ إلى نسخ إلى الحافظة نسخ القيمة + جارٍ النسخ... رمز الدولة (XX) رمز الدولة يجب أن يكون حرفين كبيرين النسخة الحالية @@ -635,6 +664,8 @@ حذف الملف حذف المجلد حذف الخط + هل تريد حذف هذه المراجعة نهائيا? + جارٍ الحذف... الوصف الوجهة تفاصيل @@ -668,6 +699,7 @@ رابط تحميل AutoJs6 تنزيل URL جارى التحميل + مسار المسودة وثيق درج درج مفتوح يحرر @@ -701,8 +733,6 @@ التمدد باءت بالفشل فشل في البناء - فشل النسخ - فشل في انشاء فشل في إنشاء مفتاح التوقيع فشل في حذف فشل في تعطيل خدمة الوصول @@ -726,11 +756,13 @@ فشل التحديث فشل في كتابة الملف ملف + الملف: %1$s تفاصيل الملف ملف موجود بالفعل مستكشف الملفات امتدادات الملف اسم الملف + اسم الملف: %1$s ملف غير موجود مسار الملف الملف \"%1$s\" ليس برنامجًا نصيًا قابلًا للتنفيذ @@ -747,6 +779,7 @@ الاسم زر عائم مجلد + المجلد: %1$s توقف إجباري خدمة المقدمة اكتمل التنسيق @@ -768,7 +801,8 @@ زر الإخفاء إخفاء هذه العقدة إخفاء العقدة ذات الإطار المماثل - التاريخ + السجل + تم مسح السجل أيقونة تجاهل تحسينات البطارية تحديثات تجاهل @@ -796,6 +830,7 @@ عنوان IP \"%1$s\" له استخدام خاص (loopback/broadcast/multicast/reserved/...)، لذلك قد لا يمكن استخدامه للاتصال بالخادم الهدف. لم يتم العثور على نسخة أحدث استجابة + العناصر: %1$d * القفز القفز إلى النهاية @@ -848,8 +883,14 @@ مسار المصدر هو نفسه مسار الوجهة، تم إلغاء النقل. انقل جميع الملفات إلى دليل جديد نقل الملف + نقل الملف الى سلة المحذوفات نقل المجلد + نقل المجلد الى سلة المحذوفات نقل إلى + فشل النقل الى سلة المحذوفات + تم النقل الى سلة المحذوفات + جارٍ النقل... + جارٍ النقل الى سلة المحذوفات... خيارات متعددة اسم تحتاج إلى تمكين خدمة الوصول @@ -867,7 +908,8 @@ لم يتم تثبيت متصفح لا يوجد محتوى لا يوجد وصف للمحتوى - لا تاريخ + لا توجد بيانات + لا يوجد سجل لا تحديثات تم تجاهلها لا توجد إدخالات سجل لنسخها لا توجد إدخالات سجل للتصدير @@ -876,6 +918,7 @@ غير الجذر لا الوصول إلى الجذر لا توجد نصوص للتوقف عن الجري + لا يوجد سجل للاصدارات لم تمنح غير مثبّت لا تظهر النشاط الرئيسي @@ -901,14 +944,17 @@ المطور الأصلي آحرون مسار الإخراج APK + استبدال اسم الحزمة اسم الحزمة (جارٍ الحساب...) كلمة المرور كلمة المرور لا يمكن أن تكون فارغة يجب أن يحتوي كلمة المرور على %d على الأقل من الأحرف معجون + المسار: %1$s المسار فارغ عنوان خادم الكمبيوتر + الاستخدام: %1$s يسمح للتطبيق بالوصول إلى معلومات الموقع التقريبية عبر الشبكة يسمح للتطبيق بالحصول على معلومات الموقع الدقيقة عبر نظام تحديد المواقع العالمي (GPS) يسمح للتطبيق بالوصول إلى أوامر إضافية من مزودي خدمات الموقع @@ -1000,9 +1046,9 @@ اسم الإدخال الرجاء الانتظار... يرجى المحاولة مرة أخرى لاحقًا... - Plugin center + مركز الاضافات تفاصيل الملحق - Plugins + الاضافات موقع المؤشر فشل تبديل \"موقع المؤشر\".\nالوصول إلى الجذر مطلوب. نشر الإخطارات @@ -1038,11 +1084,14 @@ يسجل نجح السجل التسجيل + سجل الاصدارات ملاحظات الإصدار التحكم عن بعد تم حفظ الملف البعيد في وحدة التخزين المحلية بنجاح تم حفظ المشروع البعيد في وحدة التخزين المحلية بنجاح إزالة جميع نقاط التوقف + ازالة من سلة المحذوفات + هل تريد حذف هذا العنصر من سلة المحذوفات نهائيا? إعادة تسمية يحل محل استبدل كل شيء @@ -1056,6 +1105,10 @@ إعادة التعيين هي فقط للعينات إعادة بدء استراتيجية إعادة التشغيل + استعادة + استعادة الى المسار الاصلي + استعادة الى مسار محدد + جارٍ الاستعادة... نتيجة جارٍ جلب سجل التغييرات... جارٍ جلب بيانات قائمة المكونات الإضافية... @@ -1131,7 +1184,13 @@ فرز فرز حسب آخر تحديث فرز حسب الاسم + الاسم (A-Z) فرز حسب حجم الحزمة + المسار (A-Z) + الحجم (الاصغر اولا) + الحجم (الاكبر اولا) + الوقت (الاقدم اولا) + الوقت (الاحدث اولا) المصدر مسار رمز المصدر أذونات خاصة @@ -1162,10 +1221,15 @@ مهمة توقيت محرك جدولة المهام المؤقتة توقيت + قيد الاختيار أدوات + الحجم: %1$s قد يتم تلقائيًا إخفاء رمز المشغّل بخلفية شفافة أو إضافة خلفية إليه بواسطة النظام, لذلك قد يختلف التأثير الفعلي بين الأجهزة المختلفة + سلة المحذوفات الدورة التعليمية يكتب + ملف + مجلد لا يمكن استخدام ABI المحدد \"%s\" لإنشاء تطبيق مجمع، لأن الإصدار الحالي من AutoJs6 لا يتضمن ABI أعلاه. غير متوفره واجهة برمجة التطبيقات \"%s\" غير متوفرة @@ -1174,15 +1238,15 @@ @string/text_under_development الغاء التحميل ينسحب - Uninstall + الغاء التثبيت غير معروف مؤلف غير معروف وصف غير معروف عنوان غير معروف إصدار غير معروف لم يتم التحقق - Updatable - Update + قابل للتحديث + تحديث التحديثات تم مسح الحالات التي تم فحصها في وقت لاحق @@ -1210,7 +1274,10 @@ إصدار رمز الإصدار رمز الإصدار (جارٍ الحساب...) - سجل الإصدارات + الاصدارات: %1$s + سجل الاصدارات + عند الاستعادة، يتم تغيير محتوى المحرر فقط. لن يتم الحفظ تلقائيا في الملف. يرجى الضغط على زر \"حفظ\" يدويا. + ستؤدي الاستعادة الى استبدال محتوى الملف.\nالمسار: %1$s معلومات الإصدار اسم الإصدار اسم الإصدار (جارٍ الحساب...) @@ -1225,28 +1292,4 @@ اكتب إعدادات الأمان كتابة إعدادات النظام النوافذ المنبثقة في الخلفية - فشل التحديث - فشل الحفظ - مسار المسودة - نسخ - حفظ باسم - سجل الاصدارات - لا يوجد سجل للاصدارات - عند الاستعادة، يتم تغيير محتوى المحرر فقط. لن يتم الحفظ تلقائيا في الملف. يرجى الضغط على زر \"حفظ\" يدويا. - نقل الى سلة المحذوفات - حذف نهائيا - تم النقل الى سلة المحذوفات - اختر طريقة الحذف - سلة المحذوفات - اختر مسار الاستعادة - استعادة الى المسار الاصلي - استعادة الى مسار محدد - المسار: %1$s - اسم الملف: %1$s - اختيار - استبدال - اعادة تسمية تلقائيا - لا توجد بيانات - مسار غير صالح: %1$s - ستؤدي الاستعادة الى استبدال محتوى الملف.\nالمسار: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index f427edf5..85df798a 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1,4 +1,6 @@ + + @@ -94,6 +96,7 @@ \"Pointer location\" is a debugging feature in Android Developer options.\nWhen enabled, the system will display information about touch point(s) on the screen, such as [coordinates/movement trajectory/count/size/movement speed/pressure], which helps with [writing/debugging/verification] of related scripts. The \"post notifications\" permission allows AutoJs6 to publish notifications to the system, so scripts can post and manage custom notifications in the notification shade.\n\nNote: on Android 13+ devices, if this permission is not granted, some notifications may not show, and it may affect foreground service startup and stability. With project media access, security warning for screen recording will not prompt. + View the release history and key category statistics of AutoJs6 on GitHub. The restart strategy only affects the restart button in the home page drawer.\n\nQuick restart: Quickly restarts the app. If the restart fails or unexpected situations occur, try switching to \"Scheduled restart\".\nScheduled restart: Sets up a short-timed task in advance. After the app stops, it will start again on schedule to achieve app restart. Switch ON (default): results of Java methods that are instances of Number/Boolean/Character are wrapped as Java objects and exposed to scripts (String excluded). typeof is \"object\", species is \"JavaObject\"; Java methods remain accessible, which helps retain precise Java type traits and overload resolution.\n\nSwitch OFF: the above types are no longer wrapped and are exposed directly as JavaScript primitives (number/boolean/one‑character string). typeof is the corresponding JavaScript type, aligning better with JavaScript semantics and ecosystem. You can still explicitly create a Java wrapper via new, e.g. new java.lang.Boolean(true).\n\nSee: http://issues.autojs6.com/435 If you have exotic root or abnormal state for root access, you can force set root to root or non-root. @@ -106,7 +109,6 @@ Controls the underlying mechanism for triggering scripts on a schedule.\n\nAlarmManager: With the \"Allow setting alarms and reminders\" permission granted, scheduled tasks can fire closer to the exact time.\nWorkManager: System-friendly; suitable for non-critical tasks that can tolerate some delay.\nJobScheduler: Legacy implementation kept for compatibility; higher chance of delays. Use cases and differences:\n\n1. AlarmManager\nBest for time-sensitive tasks, e.g., [reminders/strictly scheduled scripts/...].\nOn Android 12+ with the \"Allow setting alarms and reminders\" permission, tasks can run more on time even when the screen is off or the device is idle.\nWithout it, the system may degrade exactness or delay execution.\n\n2. WorkManager\nBest for tasks that don’t require strict timing, e.g., [non-critical sync/cleanup/statistics/...].\nScheduling depends on [battery/network/charging/idle policies/...], so execution may be postponed when the screen is off or idle.\nWhile it can’t guarantee exact timing, it excels at [reliable completion/retries/chaining/unique work de-duplication].\n\n3. JobScheduler\nHistorical implementation for compatibility.\nOn newer Android versions, tasks may be delayed more noticeably.\nGenerally not recommended unless required for compatibility. Provides access to device usage history and statistics which results in currentPackage() a more accurate result - View the release version history and key category statistics of AutoJs6 on GitHub. Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service). The \"write system settings\" permission allows AutoJs6 to modify some system settings, so scripts can change system parameters such as [ screen brightness / auto-rotate / screen timeout ]. Abandon @@ -117,11 +119,13 @@ @string/text_back Cancel Cancel + Choose Clear OK Connect @string/text_continue @string/text_copy + Copy path Def prefix Details Dismiss @@ -136,17 +140,20 @@ Minimize More Palette + Preview Quit + History Remove + @string/text_restore Retrieve Retry Save + Save as System settings System Update Use default Use palette - Histories View File View shortcuts View update @@ -209,7 +216,6 @@ Cannot convert %s into a ScriptRuntime path Cannot find EditorView from child view: %s Cannot get downloading URL - Failed to rename Cannot retrieve the battery state Use more characters for the password Those passwords did not match @@ -237,6 +243,8 @@ Failed to change the toggle state Failed to convert value %s into a Drawable + Failed to copy + Failed to create Failed to open the settings page Failed to grant Shizuku access Failed to instantiate \"%s\" @@ -244,10 +252,12 @@ Failed to launcher manager Failed to launch system settings Failed to load plugins.\nReason: %1$s. + Failed to move + Failed to rename Failed to render content Failed to retrieve changelog + Failed to retrieve the release history Failed to retrieve release notes - Failed to retrieve version histories Failed to retrieve version.properties to parse version information Failed to revoke Shizuku access Failed to run script @@ -271,6 +281,7 @@ Invalid GitHub repository: %s Invalid IP address Invalid login credentials + Invalid path: %1$s Invalid rest compass: %s Invalid selector map element: { %s: %s (%s) } IP address can not be empty @@ -315,11 +326,13 @@ Port number over 65535 Project main script file \"%1$s\" does not exist Cannot put value %s into JSON + Refresh failed Cannot find previous with regex Repeated colon symbol Repeated dot symbol Required property \"%1$s\" is nullish or does not exist Resolved path for \"%1$s\" cannot be null + Save failed Script is on exiting Selector method should be called rather than passed as a parameter: %s Cannot use a compass when result type is selector @@ -331,12 +344,18 @@ The transformer for required property \"%1$s\" cannot return nullish Thread is not alive Timeout while querying %1$s plugin info. + Current file path is unknown, unable to display file details + Current file path is unknown, unable to display version history + Current file path is unknown, unable to open with other apps + Current file path is unknown, unable to package application + Current file path is unknown, unable to save file Unable to use Shizuku service Unacceptable character Unknown error Unknown algorithm selector param: %s Unknown color name: %s Unknown element name: %1$s + Unknown exception Unknown gravity: %s Unknown operation Unknown result type {name: %s} @@ -344,7 +363,8 @@ Unknown selector {name: %s, type: %s} Unknown type: %s Format WebP-Lossless cannot be compressed with a quality parameter, use JPEG/PNG/WebP-Lossy instead - File does not exist or readable: %s + File does not exist or readable + File does not exist or readable: %s %1$s foreground service Click to launch %1$s %1$s foreground service @@ -359,6 +379,8 @@ 0 for infinite loop IPv4, IPv6 and domain are supported. Enter a URL pointing to a remote plugin address.\nFor example \"https://example.com/plugin.apk\". + Delete permanently + Move to trash Latest used: %1$s \"Blob\" thread request failed \"Blob\" thread request successful, writing offline cache @@ -426,12 +448,12 @@ Allow the application to create and send notifications Execute before the script file Show message when starting or stopping recording a script + View release history and statistics Java primitive types will be wrapped as Java objects in scripts More stable layout analysis but worse code compatibility (a11y service restarting needed) Add shortcuts to launcher Start or stop recording controlled by \"Volume Down\" key when floating button is showing Press \"Volume Up\" key to stop all running scripts - View release version history and statistics Internal Storage Accessibility service Required by the script automatic operation (click, long press, slide, etc.). @@ -445,8 +467,8 @@ About display over other apps Feedback Licenses + @string/text_release_history Update - @string/text_version_histories Access has been granted Click (click) Long click (longClick) @@ -463,8 +485,7 @@ All All files access \"All files access\" is needed to access script files on the phone - All histories - All histories have been cleared + All history entries All items have been cleared Granting the \"Allow setting alarms and reminders\" permission is recommended to help tasks run as punctually as possible even when the screen is off or the device is idle Copied to clipboard @@ -518,6 +539,7 @@ Auto check for updates Check for updates automatically and show a snackbar on homepage Auto night mode + Auto rename Available windows Back Action @@ -550,12 +572,18 @@ Check for updates (ignore the local version) Fetch the latest remote version directly when checking for updates Checking for updates... + Choose delete strategy + Choose restore path City or Locality Class / Package name Clear Clear file selection Clear pre-execute script + Clear trash + Clear all items in trash? Clear updates checked states + Clear version history + Clear all version history items? Click icon to add launcher shortcut Click the item to remove Click an item to view details @@ -576,10 +604,10 @@ Compatibility Confidence Config - Are you sure to clear all histories? Are you sure to clear all items? - All histories below will be cleared\n\n[ %1$s ]\n\nAre you sure to clear them all? - All histories below will be cleared\n\n[ All color libraries ]\n\nAre you sure to clear them all? + Are you sure to clear the history? + The history below will be cleared\n\n[ %1$s ]\n\nAre you sure to clear? + All history entries below will be cleared\n\n[ All color libraries ]\n\nAre you sure to clear them all? Are you sure to delete? Are you sure to remove? Are you sure to uninstall? @@ -604,6 +632,7 @@ Copy to Copy to clipboard Copy value + Copying... Country Code (XX) Country code must be two capital letters Current version @@ -630,6 +659,8 @@ Delete file Delete folder Delete line + Delete this revision permanently? + Deleting... Description Destination Details @@ -663,6 +694,7 @@ Download link for AutoJs6 Download URL Downloading + Draft file path Close drawer Open drawer Edit @@ -696,8 +728,6 @@ Extensibility Failed Failed to build - Failed to copy - Failed to create Failed to create signature key Failed to delete Failed to disable accessibility service @@ -721,11 +751,13 @@ Failed to update Failed to write file File + File: %1$s File details File already exists File explorer File extensions File name + File name: %1$s File does not exist File Path File \"%1$s\" is not an executable script @@ -742,6 +774,7 @@ Full Name Floating button Folder + Folder: %1$s Force stop Foreground service Formatting completed @@ -763,7 +796,8 @@ Hide button Hide this node Hide same frame nodes - Histories + History + The history has been cleared Icon Ignore battery optimizations Ignored updates @@ -791,6 +825,7 @@ IP address \"%1$s\" has a special purpose (loopback/broadcast/multicast/reserved/...), so it may not be usable for connecting to the target server. No newer version found Feedback + Items: %1$d *.js file Jump Jump to end @@ -843,8 +878,14 @@ Source path is the same as destination path, move aborted. Move all files to new directory Move file + Move file to trash Move folder + Move folder to trash Move to + Failed to move to trash + Moved to trash + Moving... + Moving to trash... Multiple options Name Need to enable accessibility service @@ -862,7 +903,8 @@ No browser installed No content No content description - No histories + No data + No history No ignored updates No log entries to copy No log entries to export @@ -871,6 +913,7 @@ Non-root No root access No scripts to stop running + No version history Not granted Not installed Not showing main activity @@ -896,14 +939,17 @@ Original developer Others APK output path + Overwrite Package name Package name (computing...) Password Password cannot be empty Password must be at least %d characters long Paste + Path: %1$s Path is empty PC server address + Usage: %1$s Allow the app to access approximate location via network Allow the app to access precise location via GPS Allow the app to access extra commands for location providers @@ -1033,11 +1079,14 @@ Register Register succeeded Registering + Release history Release notes remote Remote file saved to local storage successfully Remote project saved to local storage successfully Remove all breakpoints + Remove from trash + Remove this item from trash permanently? Rename Replace Replace all @@ -1051,6 +1100,10 @@ Reset is only for samples Restart Restart strategy + Restore + Restore to original path + Restore to specified path + Restoring... Result Retrieving changelog... Retrieving plugin list data... @@ -1126,7 +1179,13 @@ Sort Sort by last update time Sort by name + Name (A-Z) Sort by package size + Path (A-Z) + Size (smallest first) + Size (largest first) + Time (oldest first) + Time (newest first) Source Source code path Special permissions @@ -1157,10 +1216,15 @@ Timed task Timed task scheduling engine Timing + To be chosen Tools + Size: %1$s The transparent background launcher icon may be automatically masked or given a background by the system, so the actual effect may vary across different devices + Trash Tutorial Type + File + Folder The selected ABI \"%s\" cannot be used to create a packaged application, because the current version of AutoJs6 does not include the above ABI. Unavailable ABI \"%s\" is unavailable @@ -1205,7 +1269,10 @@ Version Version code Version code (computing...) - Version histories + Versions: %1$s + Version history + Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually. + Restore will overwrite file content.\nPath: %1$s Version info Version name Version name (computing...) @@ -1220,28 +1287,4 @@ Write security settings Write system settings Display pop-up windows while running in the background - Refresh failed - Save failed - Draft file path - Copy path - Save as - Version history - No version history - Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually. - Move to trash - Delete permanently - Moved to trash - Choose delete strategy - Trash - Choose restore path - Restore to original path - Restore to specified path - Path: %1$s - File name: %1$s - Choose - Overwrite - Auto rename - No data - Invalid path: %1$s - Restore will overwrite file content.\nPath: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 13f9ed7e..223de638 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,6 +1,8 @@ + + @@ -97,6 +99,7 @@ \"Ubicación del puntero\" es una función de depuración en las opciones de desarrollador de Android.\nAl activarla, el sistema mostrará en pantalla información del/de los punto(s) de toque, como [coordenadas/trayectoria de movimiento/cantidad/tamaño/velocidad de movimiento/presión], lo que facilita la [escritura/depuración/verificación] de los scripts relacionados. El permiso \"enviar notificaciones\" permite que AutoJs6 publique notificaciones en el sistema, de modo que los scripts puedan publicar y administrar notificaciones personalizadas en la barra de notificaciones.\n\nNote: en dispositivos con Android 13+, si este permiso no se concede, algunas notificaciones pueden no mostrarse y esto puede afectar el inicio y la estabilidad de los servicios en primer plano. Con el acceso a los medios del proyecto, la advertencia de seguridad para la grabación de la pantalla no aparecerá. + Ver el historial de versiones publicadas de AutoJs6 en GitHub y las estadísticas de las categorías importantes. La estrategia de reinicio solo afecta al botón de reinicio del cajón de la página principal.\n\nReinicio rápido: reinicia rápidamente la app. Si el reinicio falla o ocurre alguna situación inesperada, prueba cambiar a \"Reinicio programado\".\nReinicio programado: configura de antemano una tarea de corta duración. Después de que la app se detenga, se iniciará de nuevo según lo programado para lograr el reinicio de la app. Con el interruptor activado (predeterminado): los valores devueltos por métodos Java de tipo Number/Boolean/Character se envuelven como objetos Java y se exponen al script (String excluido). typeof es \"object\", species es \"JavaObject\"; se pueden invocar métodos de Java, lo que ayuda a conservar las características de tipo precisas de Java y la resolución de sobrecargas.\n\nCon el interruptor desactivado: no se envuelven los tipos anteriores y se exponen directamente como primitivos de JavaScript (number/boolean/cadena de un solo carácter). typeof corresponde al tipo de JavaScript respectivo, más acorde con la semántica/ecosistema de JavaScript. Aun así, puede declarar explícitamente un envoltorio de Java con new, por ejemplo new java.lang.Boolean(true).\n\nConsulte: http://issues.autojs6.com/435 Si tiene una raíz exótica o un estado anormal para el acceso a la raíz, puede forzar el establecimiento de la raíz a raíz o no raíz. @@ -109,7 +112,6 @@ Controla el mecanismo subyacente para activar scripts de forma programada.\n\nAlarmManager: Mediante la concesión del permiso \"Permitir configurar alarmas y recordatorios\", las tareas programadas pueden dispararse con mayor puntualidad.\nWorkManager: Amigable con el sistema; adecuado para tareas no críticas que toleran cierta demora.\nJobScheduler: Implementación antigua, mantenida por compatibilidad; la ejecución puede retrasarse con mayor probabilidad. Escenarios de uso y diferencias:\n\n1. AlarmManager\nAdecuado para tareas sensibles a la puntualidad, p. ej., [recordatorios/scripts estrictamente programados/...].\nEn Android 12+ y con el permiso \"Permitir configurar alarmas y recordatorios\", puede ejecutar tareas de forma más puntual con la pantalla apagada o en estado de espera.\nSin dicho permiso, el sistema puede degradar la exactitud o retrasar la ejecución.\n\n2. WorkManager\nAdecuado para tareas que no requieren estricta puntualidad, p. ej., [sincronización no crítica/limpieza/estadísticas/...].\nSe programa según [batería/red/carga/estrategias de reposo/...], por lo que puede posponer la ejecución con la pantalla apagada o en espera.\nAunque no garantiza precisión temporal, tiene ventajas en [finalización fiable/reintentos en caso de fallo/encadenamiento/deduplicación de trabajos].\n\n3. JobScheduler\nImplementación histórica para compatibilidad.\nEn versiones recientes de Android, las tareas pueden experimentar mayores retrasos.\nPor lo general, no se recomienda salvo por motivos de compatibilidad. Proporciona acceso al historial de uso del dispositivo y a las estadísticas, lo que da a currentPackage() un resultado más preciso - Ver el historial de versiones publicadas de AutoJs6 en GitHub y las estadísticas de las categorías importantes. Ajustes de seguridad del sistema, que contienen preferencias del sistema que las aplicaciones pueden leer pero no pueden escribir.\nSe trata de preferencias que el usuario debe modificar explícitamente a través de la interfaz de usuario de una aplicación del sistema.\nCon el permiso de configuración segura del sistema, las aplicaciones normales pueden modificar directamente la configuración segura (como el servicio de accesibilidad). El permiso \"modificar ajustes del sistema\" permite que AutoJs6 cambie algunos ajustes del sistema, de modo que los scripts puedan modificar parámetros como [ brillo de pantalla / rotación automática / tiempo de espera de pantalla ]. Abandonar @@ -120,11 +122,13 @@ @string/text_back Cancelar Cancelar + Elegir Borrar OK Conectar @string/text_continue @string/text_copy + Copiar Prefijo def Detalles Descartar @@ -139,17 +143,20 @@ Minimizar Más Abrir paleta + Vista previa Salir + Historial Eliminar + @string/text_restore Obtener Reintentar Guardar + Guardar como Configuración del sistema Sistema Actualizar Usar por defecto Usar paleta - Historial Ver archivo Ver accesos directos Ver actualización @@ -212,7 +219,6 @@ No se puede convertir %s en una ruta de ScriptRuntime No se puede encontrar el EditorView de la vista hija: %s No se puede obtener la URL de descarga - Fallo en el cambio de nombre No se puede recuperar el estado de la batería Usar más caracteres para la contraseña Las contraseñas no coinciden @@ -240,6 +246,8 @@ No se pudo cambiar el estado del interruptor Fallo al convertir el valor %s en un Dibujable + Error al copiar + Fallo al crear No se pudo abrir la página de ajustes Fallo en la concesión de permisos Shizuku Error al instanciar \"%s\" @@ -247,10 +255,12 @@ No se pudo iniciar el administrador No se pudieron abrir los ajustes del sistema Error al cargar los complementos.\nMotivo: %1$s. + No se pudo mover + Fallo en el cambio de nombre Error al renderizar el contenido No se pudo obtener el registro de cambios + No se pudo obtener el historial de lanzamientos No se pudieron obtener las notas de la versión - No se pudo obtener el historial de versiones No se pudo obtener version.properties para analizar la información de la versión Fallo en la revocación de permisos Shizuku Error al ejecutar el script @@ -274,6 +284,7 @@ Repositorio GitHub inválido: %s Dirección IP no válida Credenciales de acceso no válidas + Ruta no valida: %1$s Compás de reposo inválido: %s Elemento de mapa selector no válido: { %s: %s (%s) } La dirección IP no puede estar vacía @@ -318,11 +329,13 @@ Número de puerto superior a 65535 El archivo de script principal del proyecto \"%1$s\" no existe No se puede poner el valor %s en JSON + Error al actualizar No se puede encontrar lo anterior con regex Símbolo de dos puntos repetido Símbolo de punto repetido La propiedad requerida \"%1$s\" es nula o no existe La ruta resuelta para \"%1$s\" no puede ser nula + Error al guardar El script está al salir Se debería llamar al método selector en lugar de pasarlo como parámetro: %s No se puede usar una brújula cuando el tipo de resultado es selector @@ -334,12 +347,18 @@ El transformador para la propiedad requerida \"%1$s\" no puede devolver un valor nulo El hilo no está vivo Tiempo de espera agotado al consultar la información del plugin %1$s. + La ruta del archivo actual es desconocida; no se pueden mostrar los detalles del archivo + La ruta del archivo actual es desconocida; no se puede mostrar el historial de versiones + La ruta del archivo actual es desconocida; no se puede abrir con otras apps + La ruta del archivo actual es desconocida; no se puede empaquetar la aplicacion + La ruta del archivo actual es desconocida; no se puede guardar el archivo No se puede utilizar el servicio Shizuku Carácter inaceptable Error desconocido Parámetro de selector de algoritmo desconocido: %s Nombre de color desconocido: %s Nombre de elemento desconocido: %1$s + Excepcion desconocida Gravedad desconocida: %s Operación desconocida Tipo de resultado desconocido {nombre: %s} @@ -347,7 +366,8 @@ Selector desconocido {nombre: %s, tipo: %s} Tipo desconocido: %s El formato WebP-Lossless no se puede comprimir con un parámetro de calidad; usa JPEG/PNG/WebP-Lossy en su lugar - El archivo no existe o no se puede leer: %s + El archivo no existe o no se puede leer + El archivo no existe o no se puede leer: %s Servicio de primer plano de %1$s Haga clic para iniciar %1$s Servicio de primer plano de %1$s @@ -362,6 +382,8 @@ 0 para bucle infinito Se admiten IPv4, IPv6 y dominios. Introduce una URL que apunte a la dirección de un complemento remoto.\nPor ejemplo, \"https://example.com/plugin.apk\". + Eliminar definitivamente + Mover a la papelera Último uso: %1$s Hilo \"blob\" fallido Hilo \"blob\" exitoso, escribiendo caché sin conexión @@ -429,12 +451,12 @@ Permitir que la aplicación cree y envíe notificaciones Ejecutar antes del archivo de script Mostrar mensaje al iniciar o detener la grabación de un guion + Ver historial de lanzamientos y estadisticas Los tipos primitivos de Java se envolverán como objetos Java en los scripts Análisis de diseño más estable pero peor compatibilidad de código (se necesita reiniciar el servicio de accesibilidad) Añadir accesos directos al lanzador Iniciar o detener la grabación controlada por la tecla de bajar el volumen cuando se muestra el botón flotante Pulse la tecla \"Subir volumen\" para detener todos los scripts en ejecución - Ver el historial de versiones publicadas y las estadísticas Almacenamiento interno Servicio de accesibilidad Requerido por el funcionamiento automático del script (clic, pulsación larga, deslizamiento, etc.). @@ -448,8 +470,8 @@ Acerca de la visualización sobre otras aplicaciones Comentarios Licencias + @string/text_release_history Actualizar - @string/text_version_histories Se ha concedido el acceso Clic (click) Clic largo (longClick) @@ -466,8 +488,7 @@ Todos Acceso a todos los archivos Se necesita \"All files access\" para acceder a los archivos de script en el teléfono - Todos los historiales - Todos los historiales han sido borrados + Todas las entradas del historial Todos los elementos han sido borrados Se recomienda conceder el permiso \"Permitir configurar alarmas y recordatorios\" para asegurar que las tareas programadas se ejecuten lo más puntualmente posible incluso con la pantalla apagada o en espera Copiado en el portapapeles @@ -508,7 +529,7 @@ Docs AutoJs6 Registrar Registrar - AutoJs6 Plugin Center + Centro de plugins de AutoJs6 Plugins AutoJs6 Configuración Configuración @@ -521,6 +542,7 @@ Comprobación automática de actualizaciones Comprueba las actualizaciones automáticamente y muestra una barra de snacks en la página de inicio Modo nocturno automático + Renombrar automaticamente Ventanas disponibles Volver Acción @@ -553,17 +575,23 @@ Buscar actualizaciones (ignorar la versión local) Obtener directamente la última versión remota al buscar actualizaciones Comprobando las actualizaciones... + Elegir metodo de eliminacion + Elegir ruta de restauracion Ciudad o localidad Nombre de la clase/paquete Borrar Borrar selección de archivos Borrar script de pre-ejecución + Vaciar papelera + ¿Vaciar todos los elementos de la papelera? Borrar los estados de comprobación de las actualizaciones - Click icon to add launcher shortcut + Borrar historial de versiones + ¿Borrar todos los elementos del historial de versiones? + Toca el icono para anadir un acceso directo al lanzador Haga clic en el elemento a eliminar Toca un elemento para ver los detalles Haga clic en \"Aceptar\" para ir a la configuración - Click other areas to exit selection + Toca fuera para salir de la seleccion Funcionamiento frecuente Modo cliente Clonar biblioteca de colores @@ -579,10 +607,10 @@ Compatibilidad Confianza Configuración - ¿Está seguro de borrar todos los historiales? ¿Seguro que desea borrar todos los elementos? - Se borrarán los siguientes historiales\n\n[ %1$s ]\n\n¿Está seguro de borrarlos? - Se borrarán los historiales siguientes\n\n[ Todas las bibliotecas de colores ]\n\n¿Está seguro de borrarlos? + ¿Seguro que quieres borrar el historial? + Se borrara el historial siguiente\n\n[ %1$s ]\n\n¿Seguro que quieres borrarlo? + Se borraran todas las entradas del historial siguientes\n\n[ Todas las bibliotecas de colores ]\n\n¿Seguro que quieres borrarlas todas? ¿Seguro que quieres borrar? ¿Está seguro de eliminar? ¿Seguro que deseas desinstalar? @@ -607,6 +635,7 @@ Copiar a Copiar al portapapeles Copiar valor + Copiando... Código de país (XX) El código del país debe ser de dos letras mayúsculas Versión actual @@ -633,6 +662,8 @@ Eliminar archivo Eliminar carpeta Borrar línea + ¿Eliminar esta revision de forma permanente? + Eliminando... Descripción Destino Detalles @@ -666,6 +697,7 @@ Enlace de descarga de AutoJs6 URL de descarga Descargando + Ruta del borrador Cerrar cajón Abrir el cajón Editar @@ -699,8 +731,6 @@ Extensibilidad Falló Fallo en la construcción - Error al copiar - Fallo al crear Error al crear la clave de firma Fallo en la eliminación Fallo al desactivar el servicio de accesibilidad @@ -724,11 +754,13 @@ Error al actualizar Fallo al escribir el archivo Archivo + Archivo: %1$s Detalles del archivo El archivo ya existe Explorador de archivos Extensiones de archivo Nombre del archivo + Nombre de archivo: %1$s El archivo no existe Ruta del archivo El archivo \"%1$s\" no es un script ejecutable @@ -745,6 +777,7 @@ Nombre completo Botón flotante Carpeta + Carpeta: %1$s Forzar parada Servicio de primer plano Formateo completado @@ -766,7 +799,8 @@ Botón de ocultar Ocultar este nodo Ocultar nodo del mismo marco - Historiales + Historial + El historial se ha borrado Icono Ignorar las optimizaciones de la batería Ignorar actualizaciones @@ -794,6 +828,7 @@ La dirección IP \"%1$s\" tiene un propósito especial (loopback/broadcast/multicast/reserved/...), por lo que es posible que no pueda usarse para conectarse al servidor de destino. No se ha encontrado una versión más reciente Comentarios + Elementos: %1$d Archivo *.js Salta a Saltar al final @@ -846,8 +881,14 @@ La ruta de origen es la misma que la de destino; movimiento cancelado. Mover todos los archivos a un nuevo directorio Mover archivo + Mover archivo a la papelera Mover carpeta + Mover carpeta a la papelera Mover a + No se pudo mover a la papelera + Movido a la papelera + Moviendo... + Moviendo a la papelera... Opciones múltiples Nombre Necesidad de habilitar el servicio de accesibilidad @@ -865,7 +906,8 @@ No hay navegador instalado Sin contenido Sin descripción de contenido - No hay historiales + Sin datos + Sin historial No hay actualizaciones ignoradas No hay entradas de registro para copiar No hay entradas de registro para exportar @@ -874,6 +916,7 @@ No root Sin acceso root No hay scripts que dejen de ejecutarse + Sin historial de versiones No se concede No instalado No muestra la actividad principal @@ -899,14 +942,17 @@ Desarrollador original Otros Ruta de salida del APK + Sobrescribir Nombre del paquete Nombre del paquete (Calculando...) Contraseña La contraseña no puede estar vacía La contraseña debe tener al menos %d caracteres Pegar + Ruta: %1$s La ruta está vacía Dirección del servidor del PC + Uso: %1$s Permitir que la aplicación acceda a la ubicación aproximada a través de la red Permitir que la aplicación acceda a la ubicación precisa a través de GPS Permitir que la aplicación acceda a comandos adicionales del proveedor de ubicación @@ -998,7 +1044,7 @@ Nombre de entrada Por favor, espere... Por favor, inténtalo de nuevo más tarde... - Plugin center + Centro de plugins Detalles del complemento Plugins Ubicación del puntero @@ -1036,11 +1082,14 @@ Registro El registro se ha realizado con éxito Registro de + Historial de lanzamientos Notas de la versión remoto El archivo remoto se ha guardado correctamente en el almacenamiento local El proyecto remoto se ha guardado correctamente en el almacenamiento local Eliminar todos los puntos de interrupción + Quitar de la papelera + ¿Eliminar este elemento de la papelera de forma permanente? Cambiar el nombre de Reemplazar Reemplazar todo @@ -1054,6 +1103,10 @@ El restablecimiento es sólo para las muestras Reiniciar Estrategia de reinicio + Restaurar + Restaurar a la ruta original + Restaurar a una ruta especifica + Restaurando... Resultado Obteniendo el registro de cambios... Obteniendo datos de la lista de complementos... @@ -1129,7 +1182,13 @@ Ordenar Ordenar por última actualización Ordenar por nombre + Nombre (A-Z) Ordenar por tamaño del paquete + Ruta (A-Z) + Tamano (mas pequeno primero) + Tamano (mas grande primero) + Hora (mas antiguo primero) + Hora (mas reciente primero) Origen Ruta del código fuente Permisos especiales @@ -1160,10 +1219,15 @@ Tarea cronometrada Motor de programación de tareas temporizadas Cronometraje + Por elegir Herramientas + Tamano: %1$s El ícono de iniciador con fondo transparente puede ser enmascarado automáticamente o recibir un fondo por parte del sistema, por lo que el efecto real puede variar entre dispositivos + Papelera Tutorial Tipo + Archivo + Carpeta La ABI seleccionada \"%s\" no se puede utilizar para crear una aplicación empaquetada, porque la versión actual de AutoJs6 no incluye la ABI mencionada. No disponible ABI \"%s\" no está disponible @@ -1172,15 +1236,15 @@ @string/text_under_development Revocar Revoc - Uninstall + Desinstalar Desconocido Autor Desconocido Descripción Desconocida Título Desconocido Versión Desconocida No verificado - Updatable - Update + Actualizable + Actualizar Actualizaciones Actualizaciones comprobadas estados borrados Más adelante @@ -1208,7 +1272,10 @@ Versión Código de la versión Código de la versión (Calculando...) - Historial de versiones + Versiones: %1$s + Historial de versiones + Al restaurar, solo se cambia el contenido del editor. No se guardara automaticamente en el archivo. Debes pulsar el boton Guardar manualmente. + La restauracion sobrescribira el contenido del archivo.\nRuta: %1$s Información de la versión Nombre de la versión Nombre de la versión (Calculando...) @@ -1223,28 +1290,4 @@ Escribir la configuración de seguridad Escribir la configuración del sistema Ventanas emergentes en segundo plano - Error al actualizar - Error al guardar - Ruta del borrador - Copiar - Guardar como - Historial de versiones - Sin historial de versiones - Al restaurar, solo se cambia el contenido del editor. No se guardara automaticamente en el archivo. Debes pulsar el boton Guardar manualmente. - Mover a la papelera - Eliminar definitivamente - Movido a la papelera - Elegir metodo de eliminacion - Papelera - Elegir ruta de restauracion - Restaurar a la ruta original - Restaurar a una ruta especifica - Ruta: %1$s - Nombre de archivo: %1$s - Elegir - Sobrescribir - Renombrar automaticamente - Sin datos - Ruta no valida: %1$s - La restauracion sobrescribira el contenido del archivo.\nRuta: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5688b900..070b0779 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,6 +1,8 @@ + + @@ -97,6 +99,7 @@ \"Emplacement du pointeur\" est une fonctionnalité de débogage dans les options pour les développeurs d\'Android.\nUne fois activée, le système affiche à l\'écran des informations sur le(s) point(s) de contact, telles que [coordonnées/trajectoire de déplacement/nombre/taille/vitesse de déplacement/pression], ce qui facilite [l\'écriture/le débogage/la vérification] des scripts associés. L\'autorisation \"envoyer des notifications\" permet à AutoJs6 de publier des notifications dans le système, afin que les scripts puissent publier et gérer des notifications personnalisées dans le panneau de notifications.\n\nNote : sur les appareils Android 13+, si cette autorisation n\'est pas accordée, certaines notifications peuvent ne pas s\'afficher, et cela peut affecter le démarrage et la stabilité des services au premier plan. Avec l\'accès aux médias du projet, l\'avertissement de sécurité pour l\'enregistrement d\'écran ne sera pas demandé. + Consulter l\'historique des versions publiées d\'AutoJs6 sur GitHub ainsi que les statistiques des principales catégories. La stratégie de redémarrage n\'affecte que le bouton de redémarrage dans le tiroir de la page d\'accueil.\n\nRedémarrage rapide : redémarre rapidement l\'application. Si le redémarrage échoue ou qu\'un comportement inattendu survient, essayez de passer à \"Redémarrage planifié\".\nRedémarrage planifié : configure à l\'avance une tâche de courte durée. Après l\'arrêt de l\'application, elle redémarrera selon la planification afin de relancer l\'application. Interrupteur activé (par défaut) : les valeurs retournées par les méthodes Java de type Number/Boolean/Character sont encapsulées comme objets Java et exposées au script (String exclu). typeof vaut \"object\", species vaut \"JavaObject\" ; les méthodes Java restent accessibles, ce qui permet de conserver les caractéristiques de type précises de Java et la résolution de surcharge.\n\nInterrupteur désactivé : les types ci‑dessus ne sont plus encapsulés et sont exposés directement comme primitives JavaScript (number/boolean/chaîne à un seul caractère). typeof correspond au type JavaScript respectif, plus conforme à la sémantique/à l\'écosystème JavaScript. Il est toujours possible de déclarer explicitement un wrapper Java avec new, par ex. new java.lang.Boolean(true).\n\nVoir : http://issues.autojs6.com/435 Si vous avez un accès exotique à la racine ou un état anormal pour l\'accès à la racine, vous pouvez forcer le réglage de la racine sur racine ou non-racine. @@ -109,7 +112,6 @@ Contrôle le mécanisme sous-jacent de déclenchement programmé des scripts.\n\nAlarmManager : En accordant l\'autorisation \"Autoriser la définition d\'alarmes et de rappels\", les tâches peuvent se déclencher avec une meilleure ponctualité.\nWorkManager : Plus conciliant avec le système ; adapté aux tâches non critiques tolérant un certain délai.\nJobScheduler : Ancienne implémentation conservée pour la compatibilité, avec un risque accru de retard. Scénarios d\'usage et différences :\n\n1. AlarmManager\nConvient aux tâches sensibles à la ponctualité, p. ex. [rappels/scripts strictement planifiés/...].\nSous Android 12+ avec l\'autorisation \"Autoriser la définition d\'alarmes et de rappels\", l\'exécution reste plus ponctuelle écran éteint ou en veille.\nSans cette autorisation, le système peut réduire la précision ou retarder l\'exécution.\n\n2. WorkManager\nConvient aux tâches ne nécessitant pas une ponctualité stricte, p. ex. [synchronisation non critique/nettoyage/statistiques/...].\nL\'ordonnancement dépend de [la batterie/le réseau/la charge/les stratégies de veille/...], ce qui peut retarder l\'exécution écran éteint ou en veille.\nBien qu\'il ne garantisse pas la précision temporelle, il offre des atouts en [achèvement fiable/reprise en cas d\'échec/chaînage/déduplication des travaux].\n\n3. JobScheduler\nImplémentation historique conservée pour compatibilité.\nSur les versions récentes d\'Android, les tâches peuvent subir des retards plus importants.\nSauf besoin de compatibilité, il n\'est généralement pas recommandé. Donne accès à l\'historique et aux statistiques d\'utilisation du périphérique, ce qui donne à currentPackage() un résultat plus précis. - Consulter l\'historique des versions publiées d\'AutoJs6 sur GitHub ainsi que les statistiques des principales catégories. Paramètres de sécurité du système, contenant les préférences du système que les applications peuvent lire mais ne sont pas autorisées à écrire.\nIl s\'agit des préférences que l\'utilisateur doit explicitement modifier par le biais de l\'interface utilisateur d\'une application système.\nAvec l\'autorisation de paramètres de sécurité du système, les applications normales peuvent directement modifier les paramètres de sécurité (comme le service d\'accessibilité). L\'autorisation \"modifier les paramètres système\" permet à AutoJs6 de modifier certains paramètres système, afin que les scripts puissent changer des réglages tels que [ luminosité de l\'écran / rotation automatique / délai d\'extinction de l\'écran ]. Abandonner @@ -120,11 +122,13 @@ @string/text_back Cancel Annulation + Choisir Effacer OK Se connecter @string/text_continue @string/text_copy + Copier Préfixe def Details Quitter @@ -139,17 +143,20 @@ Réduire Plus Palette + Apercu Quit + Historique Supprimer + @string/text_restore Récupérer Retourner Enregistrer + Enregistrer sous Paramètres du système Système Mise à jour Par défaut Utiliser la palette - Historique Voir le fichier Voir les raccourcis Voir la mise à jour @@ -212,7 +219,6 @@ Impossible de convertir %s en chemin d\'exécution de script Impossible de trouver l\'EditorView depuis la vue enfant : %s Cannot get downloading URL - Failed to rename Impossible de récupérer l\'état de la batterie Utiliser plus de caractères pour le mot de passe. Ces mots de passe ne correspondent pas @@ -240,6 +246,8 @@ Échec de la modification de l\'état de l\'interrupteur Échec de la conversion de la valeur %s en un objet à dessiner + Échec de la copie + Fail to create Échec d\'ouverture de la page des paramètres Échec de l\'octroi de la permission de Shizuku Échec de l\'instanciation de \"%s\" @@ -247,10 +255,12 @@ Échec du lancement du gestionnaire Échec du lancement des paramètres système Échec du chargement des plugins.\nRaison : %1$s. + Echec du deplacement + Failed to rename Impossible d\'afficher le contenu Échec de la récupération du journal des modifications + Echec de recuperation de l\'historique des versions Échec de la récupération des notes de version - Impossible de récupérer l\'historique des versions Impossible de récupérer version.properties pour analyser les informations de version La révocation des droits de Shizuku a échoué Échec de l\'exécution du script @@ -274,6 +284,7 @@ Invalide le dépôt GitHub : %s Adresse IP invalide Crédentiels de connexion invalides + Chemin invalide : %1$s Invalid rest compass : %s Elément de carte de sélecteur non valide : { %s : %s (%s) } L\'adresse IP ne peut pas être vide @@ -318,11 +329,13 @@ Numéro de port supérieur à 65535 Le fichier de script principal du projet \"%1$s\" n\'existe pas Cannot put value %s into JSON + Echec de l\'actualisation Cannot find previous with regex Symbole deux-points répété Symbole point répété La propriété requise \"%1$s\" est nulle ou n\'existe pas Le chemin résolu pour \"%1$s\" ne peut pas être nul + Echec de l\'enregistrement Le script est en train de sortir La méthode du sélecteur doit être appelée plutôt que passée en paramètre : %s. Cannot use a compass when result type is selector @@ -334,12 +347,18 @@ Le transformateur pour la propriété requise \"%1$s\" ne peut pas retourner une valeur nulle Thread n\'est pas vivant Délai d\'attente dépassé lors de la requête des informations du plugin %1$s. + Le chemin du fichier actuel est inconnu; impossible d\'afficher les details du fichier + Le chemin du fichier actuel est inconnu; impossible d\'afficher l\'historique des versions + Le chemin du fichier actuel est inconnu; impossible d\'ouvrir avec d\'autres applis + Le chemin du fichier actuel est inconnu; impossible d\'empaqueter l\'application + Le chemin du fichier actuel est inconnu; impossible d\'enregistrer le fichier Impossible d\'utiliser le service Shizuku Caractère inacceptable Erreur inconnue Param inconnu du sélecteur d\'algorithme : %s Nom de couleur inconnu : %s Nom d\'élément inconnu : %1$s + Exception inconnue Gravité inconnue : %s Opération inconnue Unknown result type {name : %s} @@ -347,7 +366,8 @@ Sélecteur inconnu {nom : %s, type : %s} Type inconnu : %s Le format WebP-Lossless ne peut pas être compressé avec un paramètre de qualité ; utilisez plutôt JPEG/PNG/WebP-Lossy - Le fichier n\'existe pas ou n\'est pas lisible : %s + Le fichier n\'existe pas ou n\'est pas lisible + Le fichier n\'existe pas ou n\'est pas lisible : %s Service d\'avant-plan %1$s Cliquez pour lancer %1$s Service %1$s au premier plan @@ -362,6 +382,8 @@ 0 pour une boucle infinie IPv4, IPv6 et les noms de domaine sont pris en charge. Saisissez une URL pointant vers l\'adresse d\'un plugin distant.\nPar exemple \"https://example.com/plugin.apk\". + Supprimer definitivement + Deplacer vers la corbeille Dernière utilisation : %1$s Échec du thread \"blob\" Thread \"blob\" réussi, écriture du cache hors ligne @@ -429,12 +451,12 @@ Autoriser l\'application à créer et envoyer des notifications Exécuter avant le fichier script Afficher un message lors du démarrage ou de l\'arrêt de l\'enregistrement d\'un script + Voir l\'historique des versions et les statistiques Les types primitifs Java seront exposés comme objets Java dans les scripts Analyse de la mise en page plus stable mais moins bonne compatibilité du code (redémarrage du service a11y nécessaire). Ajouter des raccourcis au lanceur Démarrer ou arrêter l\'enregistrement contrôlé par la touche de réduction du volume lorsque le bouton flottant est affiché. Appuyez sur la touche \"Volume fort\" pour arrêter tous les scripts en cours d\'exécution. - Consulter l\'historique des versions publiées et les statistiques Stockage interne Service d\'accessibilité Requise par le fonctionnement automatique du script (clic, appui long, glissement, etc.). @@ -448,8 +470,8 @@ A propos de l\'affichage sur les autres apps Feedback Licences + @string/text_release_history Mise à jour - @string/text_version_histories L\'accès a été accordé Clic (click) Long clic (longClick) @@ -466,8 +488,7 @@ Tous Accès à tous les fichiers L\'option \"Accès à tous les fichiers\" est nécessaire pour accéder aux fichiers de script sur le téléphone. - Tous les historiques - Tous les historiques ont été effacés + Toutes les entrees Tous les éléments ont été effacés Il est recommandé d\'accorder l\'autorisation \"Autoriser la définition d\'alarmes et de rappels\" afin d\'assurer un déclenchement aussi ponctuel que possible, même écran éteint ou en veille Copié dans le presse-papiers @@ -508,7 +529,7 @@ Docs AutoJs6 Journal Journal - AutoJs6 Plugin Center + Centre des plugins AutoJs6 Plugins AutoJs6 Réglages Réglages @@ -521,6 +542,7 @@ Vérification automatique des mises à jour Vérification automatique des mises à jour et affichage d\'un snackbar sur la page d\'accueil Mode nuit automatique + Renommer automatiquement Fenêtres disponibles Retour Action @@ -553,17 +575,23 @@ Vérifier les mises à jour (ignorer la version locale) Récupérer directement la dernière version distante lors de la vérification des mises à jour Recherche de mises à jour... + Choisir la methode de suppression + Choisir le chemin de restauration Ville ou localité Nom de la classe ou du paquet Clear Effacer la sélection de fichiers Effacer le script de pré-exécution + Vider la corbeille + Vider tous les elements de la corbeille? Effacer les états vérifiés des mises à jour - Click icon to add launcher shortcut + Effacer l\'historique des versions + Effacer tous les elements de l\'historique des versions? + Appuyez sur l\'icone pour ajouter un raccourci au lanceur Cliquez sur l\'élément à supprimer Touchez un élément pour afficher les détails Cliquez sur \"OK\" pour accéder aux paramètres. - Click other areas to exit selection + Appuyez ailleurs pour quitter la selection Une opération fréquente Mode client Cloner une bibliothèque de couleurs @@ -579,10 +607,10 @@ Compatibilité Confiance Configuration - Êtes-vous sûr de vouloir effacer tous les historiques ? Voulez-vous vraiment effacer tous les éléments ? - Tous les historiques ci-dessous seront effacés\n\n[ %1$s ]\n\nÊtes-vous sûr de vouloir les effacer ? - Tous les historiques ci-dessous seront effacés\n\n[ Toutes les bibliothèques de couleurs ]\n\nÊtes-vous sûr de vouloir les effacer ? + Confirmer l\'effacement de l\'historique? + L\'historique ci-dessous sera efface\n\n[ %1$s ]\n\nConfirmer l\'effacement? + Toutes les entrees ci-dessous seront effacees\n\n[ Toutes les bibliotheques de couleurs ]\n\nConfirmer tout effacer? Vous êtes sûr de vouloir supprimer ? Etes-vous sûr de supprimer ? Voulez-vous vraiment désinstaller ? @@ -607,6 +635,7 @@ Copier vers Copie vers le presse-papiers Copie de la valeur + Copie... Code du pays (XX) Le code du pays doit être composé de deux lettres majuscules Version actuelle @@ -633,6 +662,8 @@ Supprimer le fichier Supprimer le dossier Supprimer la ligne + Supprimer definitivement cette revision? + Suppression... Description Destination Détails @@ -666,6 +697,7 @@ Lien de téléchargement pour AutoJs6 Url de téléchargement Téléchargement + Chemin du brouillon Fermer le tiroir Ouvrir le tiroir Editer @@ -699,8 +731,6 @@ Extensibilité Failed Failed to build - Échec de la copie - Fail to create Échec de la création de la clé de signature Échec de la suppression Failed to disable accessibility service @@ -724,11 +754,13 @@ Échec de la mise à jour Fail to write file Fichier + Fichier: %1$s Détails du fichier Fichier déjà existant Explorateur de fichiers Extensions de fichier Nom du fichier + Nom du fichier : %1$s Le fichier n\'existe pas Chemin du fichier Le fichier \"%1$s\" n\'est pas un script exécutable @@ -745,6 +777,7 @@ Nom complet Bouton flottant Dossier + Dossier: %1$s Force stop Service d\'avant-plan Formatage terminé @@ -766,7 +799,8 @@ Bouton de cacher Masquer ce nœud Masquer le nœud du même cadre - Histoires + Historique + L\'historique a ete efface Icône Ignorer les optimisations de la batterie. Ignorer les mises à jour @@ -794,6 +828,7 @@ L\'adresse IP \"%1$s\" a un usage particulier (loopback/broadcast/multicast/reserved/...), il est donc possible qu\'elle ne puisse pas être utilisée pour se connecter au serveur cible. Pas de version plus récente trouvée Réaction + Elements: %1$d *.js file Sauter Sauter à la fin @@ -846,8 +881,14 @@ Le chemin source est identique au chemin de destination; deplacement annule. Déplacer tous les fichiers dans un nouveau répertoire. Deplacer le fichier + Deplacer le fichier vers la corbeille Deplacer le dossier + Deplacer le dossier vers la corbeille Deplacer vers + Echec du deplacement vers la corbeille + Deplace vers la corbeille + Deplacement... + Deplacement vers la corbeille... Options multiples Nom Nécessité d\'activer le service d\'accessibilité @@ -865,7 +906,8 @@ Aucun navigateur n\'est installé Pas de contenu Aucune description de contenu - Aucun Histoire + Aucune donnee + Aucun historique Aucune mise à jour ignorée Aucune entrée de journal à copier Aucune entrée de journal à exporter @@ -874,6 +916,7 @@ Non-root Aucun accès root Pas de scripts à arrêter de fonctionner + Aucun historique des versions Non accordée Non installé Not showing main activity @@ -899,14 +942,17 @@ Développeur d\'origine Autres Chemin de sortie de l\'APK + Ecraser Nom du paquet Nom du paquet (en cours de calcul...) Mot de passe Le mot de passe ne peut pas être vide Le mot de passe doit contenir au moins %d caractères Coller + Chemin : %1$s Le chemin est vide Adresse du serveur du PC + Utilisation: %1$s Autoriser l\'application à accéder à une localisation approximative via le réseau Autoriser l\'application à accéder à une localisation précise via le GPS Autoriser l\'application à accéder aux commandes supplémentaires du fournisseur de localisation @@ -998,7 +1044,7 @@ Nom de l\'entrée Veuillez patienter... Veuillez réessayer cette opération plus tard... - Plugin center + Centre des plugins Détails du plugin Plugins L\'emplacement du pointeur @@ -1036,11 +1082,14 @@ Register Enregistrement réussi Register + Historique des versions Notes de version à distance Fichier distant enregistré avec succès dans la mémoire locale Projet distant sauvegardé avec succès dans le stockage local Supprimer tous les points d\'arrêt + Retirer de la corbeille + Supprimer definitivement cet element de la corbeille? Renommer Remplacer Remplacer tout @@ -1054,6 +1103,10 @@ Reset n\'est que pour les échantillons Redémarrer Stratégie de redémarrage + Restaurer + Restaurer vers le chemin d\'origine + Restaurer vers un chemin specifique + Restauration... Résultat Récupération du journal des modifications... Récupération des données de la liste des plugins... @@ -1129,7 +1182,13 @@ Trier Trier par dernière mise à jour Trier par nom + Nom (A-Z) Trier par taille du paquet + Chemin (A-Z) + Taille (plus petit d\'abord) + Taille (plus grand d\'abord) + Heure (plus ancien d\'abord) + Heure (plus recent d\'abord) Source Chemin du code source Autorisations spéciales @@ -1160,10 +1219,15 @@ Tâche chronométrée Moteur d\'ordonnancement des tâches planifiées Timing + A choisir Outils + Taille: %1$s L\'icône du lanceur à fond transparent peut être automatiquement masquée ou recevoir un fond par le système, de sorte que l\'effet réel peut varier selon les appareils + Corbeille Tutorial Type + Fichier + Dossier L\'ABI sélectionné \"%s\" ne peut pas être utilisé pour créer une application packagée, car la version actuelle d\'AutoJs6 n\'inclut pas l\'ABI ci-dessus. Indisponible L\'ABI \"%s\" n\'est pas disponible @@ -1172,15 +1236,15 @@ @string/text_under_development Révoquer Révoq - Uninstall + Desinstaller Inconnu Auteur Inconnu Description Inconnue Titre Inconnu Version Inconnue Non vérifié - Updatable - Update + Mise a jour dispo + Mettre a jour Mises à jour Mise à jour des états vérifiés et effacés Plus tard @@ -1208,7 +1272,10 @@ Version Code de version Code de version (en cours de calcul...) - Historique des versions + Versions: %1$s + Historique des versions + La restauration ne modifie que le contenu de l\'editeur. Elle n\'enregistre pas automatiquement dans le fichier. Veuillez appuyer manuellement sur le bouton Enregistrer. + La restauration ecrasera le contenu du fichier.\nChemin : %1$s Informations de la version Nom de la version Nom de la version (en cours de calcul...) @@ -1223,28 +1290,4 @@ Écrire les paramètres de sécurité. Écrire les paramètres système Fenêtres contextuelles en arrière-plan - Echec de l\'actualisation - Echec de l\'enregistrement - Chemin du brouillon - Copier - Enregistrer sous - Historique des versions - Aucun historique des versions - La restauration ne modifie que le contenu de l\'editeur. Elle n\'enregistre pas automatiquement dans le fichier. Veuillez appuyer manuellement sur le bouton Enregistrer. - Deplacer vers la corbeille - Supprimer definitivement - Deplace vers la corbeille - Choisir la methode de suppression - Corbeille - Choisir le chemin de restauration - Restaurer vers le chemin d\'origine - Restaurer vers un chemin specifique - Chemin : %1$s - Nom du fichier : %1$s - Choisir - Ecraser - Renommer automatiquement - Aucune donnee - Chemin invalide : %1$s - La restauration ecrasera le contenu du fichier.\nChemin : %1$s \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1c64a81b..41ea981c 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,6 +1,8 @@ + + @@ -98,6 +100,7 @@ \"ポインタの位置\" は Android の開発者向けオプションにあるデバッグ機能です.\n有効にすると, システムが画面上にタッチポイントの [座標/移動軌跡/数/サイズ/移動速度/圧力] などの情報を表示し, 関連スクリプトの [作成/デバッグ/検証] に役立ちます. \"通知の送信\" 権限により, AutoJs6 はシステムに通知を投稿でき, スクリプトで通知バーへカスタム通知を投稿・管理できます.\n\nNote: Android 13+ 端末でこの権限が付与されていない場合, 一部の通知が表示されない可能性があり, フォアグラウンドサービスの起動や安定性に影響することがあります. プロジェクトのメディアアクセス権を使用すると, 画面録画のセキュリティ警告が表示されなくなります + GitHub 上の AutoJs6 のリリース版変更履歴と主要カテゴリの統計データを確認する. 再起動戦略はホームページのドロワーにある再起動ボタンにのみ適用されます.\n\nクイック再起動: アプリをすばやく再起動します. 再起動に失敗したり予期しない状況が発生した場合は/\"スケジュール再起動\" に切り替えてください.\nスケジュール再起動: あらかじめ短時間のタイマーを設定します. アプリが停止した後/スケジュールに従って再度起動し/アプリの再起動を実現します. スイッチ有効 (デフォルト): Java メソッドが返す Number/Boolean/Character は Java オブジェクトとしてラップされ/スクリプトに公開されます (String は除外). typeof は \"object\"/species は \"JavaObject\". Java のメソッドを呼び出せるため/Java の厳密な型特性やオーバーロード解決を保持しやすくなります.\n\nスイッチ無効: 上記の型はラップされず/JavaScript のプリミティブ値 (number/boolean/1 文字の文字列) として直接公開されます. typeof は対応する JavaScript 型となり/JavaScript のセマンティクス/エコシステムにより沿います. 必要なら new によって明示的に Java ラッパーを生成できます (例: new java.lang.Boolean(true)).\n\n参照: http://issues.autojs6.com/435 異国の root や root アクセスに異常がある場合, root を強制的に root または non-root に設定することができます @@ -110,7 +113,6 @@ スクリプトを定時で起動するための基盤メカニズムを制御します.\n\nAlarmManager: システムの \"アラームとリマインダーの設定を許可\" 権限を付与すると/より正確な時刻での実行に近づきます.\nWorkManager: システムに優しく/一定の遅延を許容できる非重要タスクに適しています.\nJobScheduler: 旧来の実装で互換性のために残されていますが/遅延が発生しやすいです. 利用シーンと違い: \n\n1. AlarmManager\n[リマインダー/厳密な時刻のスクリプト/...] など/時刻精度が重要なタスクに適しています.\nAndroid 12 以降で \"アラームとリマインダーの設定を許可\" 権限がある場合/画面消灯/待機中でも比較的正確に実行されます.\n権限がない場合/システムにより不正確または遅延実行になる可能性があります.\n\n2. WorkManager\n[非重要な同期/クリーンアップ/統計/...] など/厳密な時刻精度を必要としないタスクに適しています.\n[バッテリー/ネットワーク/充電状況/待機ポリシー/...] に応じてスケジューリングされ/消灯/待機中は実行が遅れることがあります.\n時間精度は保証されませんが/[確実な完了/失敗時の再試行/チェーン実行/一意タスクの重複排除] に強みがあります.\n\n3. JobScheduler\n互換性維持のための歴史的実装です.\n新しい Android では遅延が大きくなりやすい傾向があります.\n互換性の必要がない限り/通常は非推奨です. デバイスの使用履歴や統計情報にアクセスできるようになり, currentPackage()の結果がより正確になります - GitHub 上の AutoJs6 のリリース版変更履歴と主要カテゴリの統計データを確認する. アプリケーションが読み取ることはできるが, 書き込むことはできないシステム環境設定を含む, 安全なシステム設定です.\nこれは, ユーザーがシステムアプリの UI を通じて明示的に変更する必要がある環境設定のためのものです.\nセキュアなシステム設定を許可すると, 通常のアプリケーションはセキュアな設定 (アクセシビリティサービスなど) を直接変更できるようになります \"システム設定の変更\" 権限により, AutoJs6 は一部のシステム設定を変更でき, スクリプトで [ 画面の明るさ / 自動回転 / 画面タイムアウト ] などを調整できます. 放棄 @@ -121,11 +123,13 @@ @string/text_back 取消 取消 + 選択 クリア OK 接続 @string/text_continue @string/text_copy + コピー デフォ前置 詳細 削除 @@ -140,17 +144,20 @@ 最小化 詳細 パレットを開く + プレビュー 終了する + 履歴 削除 + @string/text_restore 取得 再試行 保存する + 名前を付けて保存 システム設定 システム 更新 デフォルトを使用 パレットを使う - 更新履歴 ファイルの表示 ショートカットを表示 更新を表示 @@ -213,7 +220,6 @@ %s をスクリプトランタイムパスに変換できません \"%s 子ビューから EditorView を見つけることができません ダウンロード用 URL を取得できません - 名前の変更に失敗しました バッテリー状態の取得ができません パスワードにもっと多くの文字を使用する これらのパスワードは一致しませんでした @@ -241,6 +247,8 @@ スイッチの状態の変更に失敗しました 値 %s を Drawable に変換するのに失敗しました + コピーに失敗しました + 作成に失敗しました 設定画面を開けませんでした Shizuku の権限付与に失敗しました \"%s\" のインスタンス化に失敗しました @@ -248,10 +256,12 @@ マネージャーを起動できませんでした システム設定を起動できませんでした プラグインの読み込みに失敗しました.\n理由: %1$s. + 移動に失敗しました + 名前の変更に失敗しました コンテンツの表示に失敗しました 更新ログの取得に失敗しました + リリース履歴の取得に失敗しました リリースノートの取得に失敗しました - バージョン履歴を取得できません version.properties を取得できず, バージョン情報を解析できません Shizuku 権限の取り消しに失敗しました スクリプトの実行に失敗しました @@ -275,6 +285,7 @@ 無効な GitHub リポジトリ: %s 無効な IP アドレス 無効なログイン認証情報 + 無効なパス: %1$s 無効な残りコンパスです: %s 無効なセレクタ・マップ要素です. { %s: %s (%s) } です IP アドレスは空であってはならない @@ -319,11 +330,13 @@ 65535 を超えるポート番号 プロジェクトのメインスクリプトファイル \"%1$s\" が存在しません \"値 %s を JSON に入れることができません\" というメッセージが表示されます + 更新に失敗しました 正規表現で前を見つけることができません 繰り返されるコロン記号 ドット記号の繰り返し 必要なプロパティ \"%1$s\" が null か存在しません \"%1$s\" の解決済みパスは null にできません + 保存に失敗しました スクリプトが終了してしまう セレクタ・メソッドはパラメータとして渡されるのではなく, 呼び出される必要があります: %s 結果タイプがセレクタの場合, コンパスを使用できません @@ -335,12 +348,18 @@ 必要なプロパティ \"%1$s\" のトランスフォーマーは null を返すことができません スレッドが生きていない %1$s プラグイン情報の取得がタイムアウトしました. + 現在のファイルパスが不明のため, ファイル詳細を表示できません + 現在のファイルパスが不明のため, バージョン履歴を表示できません + 現在のファイルパスが不明のため, 他のアプリで開けません + 現在のファイルパスが不明のため, アプリをパッケージ化できません + 現在のファイルパスが不明のため, ファイルを保存できません Shizuku サービスが使用できない 使用できない文字 不明なエラー 不明なアルゴリズム・セレクタ・パラメータ: %s 不明な色名: %s 不明な要素名: %1$s + 不明な例外 不明な重力: %s 不明な操作 不明な結果タイプ {name: %s} です @@ -348,7 +367,8 @@ 不明なセレクタ {name: %s, type: %s} です 不明なタイプ: %s WebP-Lossless 形式は品質パラメータで圧縮できません, 代わりに JPEG/PNG/WebP-Lossy を使用してください - ファイルが存在しないか, 読み取り可能でない: %s + ファイルが存在しないか, 読み取れません + ファイルが存在しないか, 読み取り可能でない: %s %1$s のフォアグラウンド サービス クリックすると, %1$s が起動します %1$s のフォアグラウンド・サービス @@ -363,6 +383,8 @@ 無限ループの場合は 0 IPv4/IPv6/およびドメイン名に対応しています. リモートプラグインの URL を入力してください. \n例: \"https://example.com/plugin.apk\" + 完全に削除 + ゴミ箱に移動 最終使用時: %1$s \"Blob\" スレッドが失敗 \"Blob\" スレッドが成功, オフラインキャッシュを書き込み @@ -430,12 +452,12 @@ アプリが通知を作成して送信することを許可する スクリプトファイルの前に実行する スクリプトの録音を開始または停止するときにメッセージを表示する + リリース履歴と統計を表示 Java のプリミティブ型はスクリプト内で Java オブジェクトとしてラップされます レイアウト解析はより安定するが, コードの互換性は低下 (アクセシビリティ・サービスの再起動が必要) ランチャーへのショートカットの追加 フローティングボタンが表示されているときに, \"Volume Down\" キーで録画の開始/停止を制御できるようになりました \"Volume Up\" キーを押すと, 実行中のスクリプトをすべて停止します - リリース版の変更履歴と統計データを確認する 内部ストレージ アクセシビリティサービス スクリプトの自動操作 (クリック, 長押し, スライドなど) で必要です @@ -449,8 +471,8 @@ 他のアプリとの比較表示について フィードバック ライセンスについて + @string/text_release_history 更新情報 - @string/text_version_histories アクセス権が付与されました クリック (click) ロングクリック (longClick) @@ -467,8 +489,7 @@ すべて 全ファイルアクセス 携帯電話上のスクリプトファイルにアクセスするために, \"すべてのファイルへのアクセス\" が必要です - すべての履歴 - すべての履歴がクリアされました + すべての履歴 すべての項目を削除しました 画面消灯中や待機時でもできるだけ正確に実行できるよう/[アラームとリマインダーの設定を許可] 権限の付与を推奨します クリップボードにコピーされる @@ -509,8 +530,8 @@ 文書 AutoJs6 ログ ログ - AutoJs6 Plugin Center - Plugins + AutoJs6 プラグインセンター + プラグイン AutoJs6 設定 設定 ソースコード @@ -522,6 +543,7 @@ 更新の自動チェック 自動的に更新をチェックし, ホームページにスナックバーを表示します オートナイトモード + 自動リネーム 使用可能なウィンドウ 戻る 動作 @@ -554,17 +576,23 @@ アップデートの確認 (ローカルバージョンを無視する) アップデートを確認する際に, 最新のリモートバージョンを直接取得する アップデートを確認中... + 削除方法を選択 + 復元先を選択 市区町村 クラス名/パッケージ名 クリア ファイル選択の解除 実行前スクリプトのクリア + ゴミ箱を空にする + ゴミ箱内の項目をすべて削除しますか? アップデートのチェック状態をクリアする - Click icon to add launcher shortcut + バージョン履歴を消去 + バージョン履歴をすべて消去しますか? + アイコンをタップしてランチャーにショートカットを追加 削除する項目をクリックします 項目をタップして詳細を表示します \"OK\" をクリックすると設定に進みます - Click other areas to exit selection + 他の場所をタップして選択を終了 頻繁に行う操作 クライアントモード カラーライブラリを複製 @@ -580,10 +608,10 @@ 互換性 信頼度 設定 - すべての履歴をクリアしてもよろしいですか? すべての項目を削除しますか? - 以下の履歴がクリアされます\n\n[ %1$s ]\n\n本当にクリアしてもよろしいですか? - 以下の履歴がクリアされます\n\n[ すべてのカラーライブラリ ]\n\n本当にクリアしてもよろしいですか? + 履歴を消去しますか? + 以下の履歴を消去します\n\n[ %1$s ]\n\n消去しますか? + 以下の履歴をすべて消去します\n\n[ すべてのカラーライブラリ ]\n\nすべて消去しますか? 本当に削除しますか? 本当に削除しますか? アンインストールしてもよろしいですか? @@ -608,6 +636,7 @@ コピー先 クリップボードにコピーする 値をコピーする + コピー中... 国コード (XX) 国コードは 2 文字の大文字でなければなりません 現在のバージョン @@ -634,6 +663,8 @@ ファイルを削除 フォルダーを削除 行削除 + この履歴を完全に削除しますか? + 削除中... 説明 宛先 詳細 @@ -667,6 +698,7 @@ AutoJs6 のダウンロードリンク ダウンロード URL ダウンロード中 + 下書きのパス ドロワーを閉じる ドロワーを開く 編集する @@ -700,8 +732,6 @@ 拡張性 失敗 ビルドに失敗しました - コピーに失敗しました - 作成に失敗しました 署名キーの作成に失敗しました 削除の失敗 アクセシビリティ・サービスの無効化の失敗 @@ -725,11 +755,13 @@ 更新に失敗しました ファイルの書き込みに失敗しました ファイル + ファイル: %1$s ファイルの詳細 ファイルは既に存在しています ファイルエクスプローラー ファイル拡張子 ファイル名 + ファイル名: %1$s ファイルが存在しません ファイルパス ファイル \"%1$s\" は実行可能なスクリプトではありません @@ -746,6 +778,7 @@ 氏名 フローティングボタン フォルダー + フォルダー: %1$s 強制停止 フォアグラウンドサービス フォーマット完了 @@ -767,7 +800,8 @@ 隠しボタン このノードを隠す 同じフレームのノードを隠す - ヒストリー + 履歴 + 履歴を消去しました アイコン バッテリーの最適化を無視する アップデートを無視する @@ -795,6 +829,7 @@ IP アドレス \"%1$s\" は特殊用途 (loopback/broadcast/multicast/reserved/...) のため, 接続先サーバーへの接続に使用できない可能性があります. 新しいバージョンが見つかりません フィードバック + 項目: %1$d *.js ファイル ジャンプ 末尾へジャンプ @@ -847,8 +882,14 @@ ソースパスと宛先パスが同じです. 移動を中止しました. すべてのファイルを新しいディレクトリに移動する ファイルを移動 + ファイルをゴミ箱に移動 フォルダーを移動 + フォルダーをゴミ箱に移動 移動先 + ゴミ箱への移動に失敗しました + ゴミ箱に移動しました + 移動中... + ゴミ箱に移動中... 複数のオプション 名称 アクセシビリティサービスを有効にする必要があります @@ -866,7 +907,8 @@ ブラウザがインストールされていない 内容なし コンテンツの説明がありません - 履歴がない + データなし + 履歴なし 無視された更新がない コピーするログ・エントリがありません エクスポートするログ・エントリがありません @@ -875,6 +917,7 @@ 非ルート ルートアクセス不可 実行を停止するスクリプトなし + バージョン履歴なし 許可されない 未インストール メインのアクティビティが表示されない @@ -900,14 +943,17 @@ オリジナル開発者 その他 APK 出力パス + 上書き パッケージ名 パッケージ名 (計算中...) パスワード パスワードは空であってはならない パスワードは少なくとも %d 文字以上である必要があります 貼り付け + パス: %1$s パスが空です PC サーバーのアドレス + 使用率: %1$s アプリがネットワーク経由で大まかな位置情報にアクセスすることを許可します アプリが GPS 経由で正確な位置情報を取得することを許可します アプリが位置情報プロバイダからの追加コマンドにアクセスすることを許可します @@ -999,9 +1045,9 @@ 入力名 しばらくお待ちください... しばらくしてからもう一度お試しください... - Plugin center + プラグインセンター プラグイン詳細 - Plugins + プラグイン ポインターの位置 トグル [ポインターの位置] に失敗しました.\nroot 権限が必要です ゆうびんけいほう @@ -1037,11 +1083,14 @@ 登録 登録に成功した 登録中 + リリース履歴 リリースノート リモート リモート・ファイルがローカル・ストレージに正常に保存された リモートプロジェクトがローカルストレージに正常に保存された すべてのブレークポイントを削除する + ゴミ箱から削除 + この項目をゴミ箱から完全に削除しますか? 名前の変更 置換 すべてを置き換える @@ -1055,6 +1104,10 @@ リセットはサンプルのみ 再起動 再起動戦略 + 復元 + 元のパスに復元 + 指定したパスに復元 + 復元中... 結果 更新ログを取得しています... プラグイン一覧のデータを取得しています... @@ -1130,7 +1183,13 @@ 並べ替え 最終更新日で並べ替え 名前で並べ替え + 名前 (A-Z) パッケージサイズで並べ替え + パス (A-Z) + サイズ (小さい順) + サイズ (大きい順) + 時間 (古い順) + 時間 (新しい順) ソース ソースコードのパス 特殊な権限 @@ -1161,10 +1220,15 @@ 時間指定タスク 定時タスクのスケジューリングエンジン タイミング + 未選択 ツール + サイズ: %1$s 透過背景ランチャーアイコンはシステムによって自動的にマスクされたり背景が追加されたりする場合があるため, 実際の効果はデバイスによって異なる場合があります + ゴミ箱 チュートリアル 種類 + ファイル + フォルダー 現在のバージョンの AutoJs6 には上記の ABI が含まれていないため, 選択した ABI \"%s\" を使用してパッケージ・アプリケーションを作成できません 使用できません ABI \"%s\" は使用できません @@ -1173,15 +1237,15 @@ @string/text_under_development 元に戻す 戻る - Uninstall + アンインストール 不明 不明な作者 不明な説明 不明なタイトル 不明なバージョン 未確認 - Updatable - Update + 更新あり + 更新 アップデート 更新のチェック状態を解除 後日 @@ -1209,7 +1273,10 @@ バージョン バージョンコード バージョンコード (計算中...) - バージョン履歴 + バージョン: %1$s + バージョン履歴 + 復元すると編集内容のみが変更されます. ファイルには自動保存されません. 保存ボタンを手動でタップしてください. + 復元するとファイル内容が上書きされます.\nパス: %1$s バージョン情報 バージョン名 バージョン名 (計算中...) @@ -1224,28 +1291,4 @@ セキュリティ設定の書き込み システム設定の書き込み バックグラウンドでのポップアップ表示 - 更新に失敗しました - 保存に失敗しました - 下書きのパス - コピー - 名前を付けて保存 - バージョン履歴 - バージョン履歴なし - 復元すると編集内容のみが変更されます. ファイルには自動保存されません. 保存ボタンを手動でタップしてください. - ゴミ箱に移動 - 完全に削除 - ゴミ箱に移動しました - 削除方法を選択 - ゴミ箱 - 復元先を選択 - 元のパスに復元 - 指定したパスに復元 - パス: %1$s - ファイル名: %1$s - 選択 - 上書き - 自動リネーム - データなし - 無効なパス: %1$s - 復元するとファイル内容が上書きされます.\nパス: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0e53a346..21133484 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2,6 +2,8 @@ + + @@ -99,6 +101,7 @@ \"포인터 위치\" 는 Android 개발자 옵션에 있는 디버깅 기능입니다.\n활성화하면 시스템이 화면에 터치 지점의 [좌표/이동 궤적/개수/크기/이동 속도/압력] 등의 정보를 표시하여 관련 스크립트의 [작성/디버깅/검증] 에 도움이 됩니다. \"알림 전송\" 권한은 AutoJs6 가 시스템에 알림을 게시할 수 있게 하여, 스크립트가 알림 영역에서 사용자 지정 알림을 게시하고 관리할 수 있도록 합니다.\n\nNote: Android 13+ 기기에서 이 권한이 부여되지 않으면 일부 알림이 표시되지 않을 수 있으며, 포그라운드 서비스의 시작 및 안정성에 영향을 줄 수 있습니다. 프로젝트 미디어 액세스를 사용하면 화면 기록에 대한 보안 경고가 중요하지 않습니다. + GitHub 에서 AutoJs6 릴리스 버전의 변경 기록과 주요 분류 통계 데이터를 확인하기. 재시작 전략은 홈 페이지 드로어의 재시작 버튼에만 적용됩니다.\n\n빠른 재시작: 앱을 빠르게 다시 시작합니다. 재시작이 실패하거나 예기치 않은 상황이 발생하면 \"예약 재시작\"으로 전환해 보세요.\n예약 재시작: 미리 짧은 시간의 예약 작업을 설정합니다. 앱이 중지된 후 예약에 따라 다시 시작하여 앱 재시작을 수행합니다. 스위치 활성화(기본값): Java 메서드가 반환하는 Number/Boolean/Character 값은 Java 객체로 래핑되어 스크립트에 노출됩니다(String 제외). typeof 는 \"object\", species 는 \"JavaObject\"이며, Java 메서드 호출이 가능해 Java의 정밀한 타입 특성과 오버로드 해석을 보존하는 데 도움이 됩니다.\n\n스위치 비활성화: 위 타입들을 더 이상 래핑하지 않고 JavaScript 원시 값(number/boolean/한 글자 문자열)으로 직접 노출합니다. typeof 는 해당 JavaScript 타입이 되며, JavaScript의 의미론/생태계에 더 가깝습니다. 필요 시 new 키워드로 Java 래퍼를 명시적으로 생성할 수 있습니다(예: new java.lang.Boolean(true)).\n\n참고: http://issues.autojs6.com/435 뿌리 접근을 위해 이국적인 뿌리 또는 비정상 상태가있는 경우 루트를 루트 또는 뿌리가 아닌 것으로 강제로 설정할 수 있습니다. @@ -111,7 +114,6 @@ 스크립트를 정해진 시간에 실행하는 기본 메커니즘을 제어합니다.\n\nAlarmManager: 시스템의 \"알람 및 알림 설정 허용\" 권한을 부여하면 보다 정확한 시각에 가깝게 실행됩니다.\nWorkManager: 시스템 친화적이며, 일정 지연을 허용할 수 있는 비중요 작업에 적합합니다.\nJobScheduler: 호환성 유지를 위해 남겨둔 구방식으로, 지연 발생 가능성이 큽니다. 사용 시나리오 및 차이점:\n\n1. AlarmManager\n[리마인더/엄격한 정시 스크립트/...] 등 시간 정확도가 중요한 작업에 적합합니다.\nAndroid 12+ 에서 \"알람 및 알림 설정 허용\" 권한이 있으면, 화면 꺼짐/대기 중에도 비교적 정확하게 실행됩니다.\n 권한이 없으면 시스템이 정확도를 낮추거나 실행을 지연할 수 있습니다.\n\n2. WorkManager\n[비중요 동기화/정리/통계/...] 등 엄격한 정시성이 필요 없는 작업에 적합합니다.\n[배터리/네트워크/충전 상태/대기 정책/...] 에 따라 스케줄링되며, 화면 꺼짐/대기 중에는 지연될 수 있습니다.\n 시간 정확도는 보장되지 않지만, [신뢰성 있는 완료/실패 재시도/체이닝/단일 작업 중복 방지] 에 장점이 있습니다.\n\n3. JobScheduler\n 호환성 유지를 위한 기존 구현입니다.\n 신형 Android 에서는 지연이 더 커질 수 있습니다.\n 특별한 호환성 목적이 아니라면 일반적으로 권장하지 않습니다. 장치 사용 기록 및 통계에 대한 액세스를 제공하여 CurrentPackage ()보다 정확한 결과를 초래합니다. - GitHub 에서 AutoJs6 릴리스 버전의 변경 기록과 주요 분류 통계 데이터를 확인하기. 애플리케이션이 읽을 수 있지만 쓸 수없는 시스템 환경 설정을 포함하는 보안 시스템 설정.\n이들은 사용자가 시스템 앱의 UI 를 통해 명시 적으로 수정 해야하는 선호도입니다.\n보안 시스템 설정 권한을 사용하면 일반 애플리케이션이 보안 설정 (예: 접근성 서비스)을 직접 수정할 수 있습니다. \"시스템 설정 수정\" 권한은 AutoJs6 가 일부 시스템 설정을 변경할 수 있게 하여, 스크립트가 [ 화면 밝기 / 자동 회전 / 화면 시간 제한 ] 등의 설정을 변경할 수 있도록 합니다. 포기 @@ -122,11 +124,13 @@ @string/text_back 취소 취소 + 선택 지우기 확인 연결 @string/text_continue @string/text_copy + 복사 기본 접두사 세부 닫기 @@ -141,17 +145,20 @@ 최소화 팔레트 열기 + 미리보기 그만두다 + 기록 제거하다 + @string/text_restore 가져오기 다시 해 보다 구하다 + 다른 이름으로 저장 환경 설정 시스템 업데이트 기본값을 사용하십시오 팔레트 사용 - 변경내역 파일 보기 바로가기 보기 업데이트 보기 @@ -214,7 +221,6 @@ %s 를 스크립트 런타임 경로로 변환할 수 없습니다 Child View: %s 에서 EditorView 를 찾을 수 없습니다 URL 다운로드를받을 수 없습니다 - 이름을 바꾸지 못했습니다 배터리 상태를 검색 할 수 없습니다 비밀번호에 더 많은 문자를 사용하십시오 그 암호는 일치하지 않았다 @@ -242,6 +248,8 @@ 스위치 상태 변경에 실패했습니다 %s 값을 Drawable 로 변환하지 못했습니다 + 복사하지 못했습니다 + 만들지 못했습니다 설정 페이지를 열지 못했습니다 Shizuku 권한을 부여하지 못했습니다 \"%s\" 인스턴스 생성에 실패했습니다 @@ -249,10 +257,12 @@ 관리자를 실행하지 못했습니다 시스템 설정을 실행하지 못했습니다 플러그인을 불러오지 못했습니다.\n사유: %1$s. + 이동에 실패했습니다 + 이름을 바꾸지 못했습니다 콘텐츠 렌더링에 실패했습니다 변경 로그를 가져오지 못했습니다 + 릴리스 기록을 가져오지 못했습니다 릴리스 노트를 가져오지 못했습니다 - 버전 기록을 가져올 수 없습니다 version.properties 를 가져올 수 없어 버전 정보를 분석할 수 없습니다 Shizuku 권한 해지에 실패했습니다 스크립트 실행 실패 @@ -276,6 +286,7 @@ 유효하지 않은 GitHub 저장소: %s 잘못된 IP 주소 로그인 자격 증명이 잘못되었습니다 + 잘못된 경로: %1$s 잘못된 휴식 나침반: %s 잘못된 선택기 맵 요소: { %s: %s (%s) } IP 주소는 비워둘 수 없습니다 @@ -320,11 +331,13 @@ 65535 이상의 포트 번호 프로젝트 메인 스크립트 파일 \"%1$s\" 이 존재하지 않습니다 가치 %s 를 JSON 에 넣을 수 없습니다 + 새로고침에 실패했습니다 Regex 와 함께 이전을 찾을 수 없습니다 반복 콜론 기호 반복되는 점 기호 필수 속성 \"%1$s\" 가 null 이거나 존재하지 않습니다 \"%1$s\" 에 대한 해결된 경로는 null 일 수 없습니다 + 저장에 실패했습니다 스크립트가 종료 중입니다 셀렉터 메소드는 매개 변수로 전달되지 않고 호출되어야합니다: %s 결과 유형이 선택기 일 때 나침반을 사용할 수 없습니다 @@ -336,12 +349,18 @@ 필수 속성 \"%1$s\" 에 대한 변환기는 null 값을 반환할 수 없습니다 스레드는 살아 있지 않습니다 %1$s 플러그인 정보 조회가 시간 초과되었습니다. + 현재 파일 경로를 알 수 없어 파일 정보를 표시할 수 없습니다 + 현재 파일 경로를 알 수 없어 버전 기록을 표시할 수 없습니다 + 현재 파일 경로를 알 수 없어 다른 앱으로 열 수 없습니다 + 현재 파일 경로를 알 수 없어 앱을 패키징할 수 없습니다 + 현재 파일 경로를 알 수 없어 파일을 저장할 수 없습니다 Shizuku 서비스를 사용할 수 없음 허용되지 않는 문자 알수없는 오류 알 수없는 알고리즘 선택기 매개 변수: %s 알 수 없는 색상 이름: %s 알 수 없는 요소 이름: %1$s + 알 수 없는 예외 알 수 없는 중력: %s 알 수없는 작업 알 수없는 결과 유형 {이름: %s} @@ -349,7 +368,8 @@ 알 수없는 선택기 {이름: %s, 유형: %s} 알 수없는 유형: %s WebP-Lossless 형식은 품질 파라미터로 압축할 수 없습니다, 대신 JPEG/PNG/WebP-Lossy를 사용하세요 - 파일이 존재하지 않거나 읽을 수 없습니다: %s + 파일이 없거나 읽을 수 없습니다 + 파일이 존재하지 않거나 읽을 수 없습니다: %s %1$s 전경 서비스 %1$s 을 시작하려면 클릭하십시오 %1$s 전경 서비스 @@ -364,6 +384,8 @@ 무한 루프의 경우 0 IPv4, IPv6 및 도메인을 지원합니다. 원격 플러그인 주소를 가리키는 URL을 입력하세요.\n예: \"https://example.com/plugin.apk\" + 영구 삭제 + 휴지통으로 이동 최종 사용: %1$s \"Blob\" 스레드 요청 실패 \"Blob\" 스레드 요청 성공, 오프라인 캐시 기록 @@ -431,12 +453,12 @@ 앱이 알림을 생성하고 보낼 수 있도록 허용합니다 스크립트 파일 전에 실행하십시오 스크립트 기록을 시작하거나 중지할 때 메시지 표시 + 릴리스 기록과 통계를 보기 Java 원시 타입이 스크립트에서 Java 객체로 래핑됩니다 보다 안정적인 레이아웃 분석이지만 더 나쁜 코드 호환성 (접근성 서비스 재시작 필요) 런처에 바로 가기 추가 부동 버튼이 표시 될 때 볼륨 다운 키로 제어 된 녹음 시작 또는 중지 실행 중인 모든 스크립트를 중지하려면 \"볼륨 크게\" 키를 누르십시오. - 릴리스 버전의 변경 기록 및 통계 데이터를 확인하기 내부 저장소 접근성 서비스 스크립트 자동 작동에 필요합니다 (클릭, 긴 프레스, 슬라이드 등). @@ -450,8 +472,8 @@ 다른 앱에 대한 디스플레이 정보 피드백 라이센스 + @string/text_release_history 업데이트 - @string/text_version_histories 액세스가 승인되었습니다 클릭 (click) 긴 클릭 (longClick) @@ -468,8 +490,7 @@ 전체 모든 파일에 액세스합니다 \"모든 파일 액세스\"는 전화기의 스크립트 파일에 액세스하려면 필요합니다. - 모든 기록 - 모든 기록이 삭제되었습니다 + 모든 기록 항목 모든 항목이 삭제되었습니다 화면이 꺼져 있거나 대기 상태에서도 최대한 정시에 실행되도록, \"알람 및 알림 설정 허용\" 권한 부여를 권장합니다 클립 보드에 복사 @@ -510,8 +531,8 @@ 문서 AutoJs6 통나무 통나무 - AutoJs6 Plugin Center - Plugins + AutoJs6 플러그인 센터 + 플러그인 AutoJs6 설정 설정 소스 코드 @@ -523,6 +544,7 @@ 자동 확인 업데이트 자동으로 업데이트를 확인하고 홈페이지에 스낵바를 표시합니다. 자동 밤 모드 + 자동 이름 변경 사용 가능한 창 동작 @@ -555,17 +577,23 @@ 업데이트 확인 (로컬 버전 무시) 업데이트 확인 시 최신 원격 버전을 직접 가져옵니다 업데이트 확인 ... + 삭제 방식 선택 + 복원 경로 선택 시 또는 지역 클래스 / 패키지 이름 분명한 파일 선택을 지우십시오 사전 에코 슈트 스크립트를 지우십시오 + 휴지통 비우기 + 휴지통의 모든 항목을 비울까요? 확인 된 상태에서 명확한 업데이트 - Click icon to add launcher shortcut + 버전 기록 삭제 + 버전 기록의 모든 항목을 삭제할까요? + 아이콘을 눌러 런처에 바로가기를 추가 제거하려면 항목을 클릭하십시오 항목을 탭하면 자세한 정보를 볼 수 있습니다 \"확인\"을 클릭하여 설정으로 이동하십시오 - Click other areas to exit selection + 다른 곳을 눌러 선택을 종료 빈번한 작동 클라이언트 모드 색상 라이브러리 복제 @@ -581,10 +609,10 @@ 호환성 신뢰도 구성 - 모든 기록을 삭제하시겠습니까? 모든 항목을 삭제하시겠습니까? - 다음 기록들이 삭제됩니다\n\n[ %1$s ]\n\n정말 삭제하시겠습니까? - 다음 기록들이 삭제됩니다\n\n[ 모든 색상 라이브러리 ]\n\n정말 삭제하시겠습니까? + 기록을 삭제하시겠습니까? + 아래 기록이 삭제됩니다\n\n[ %1$s ]\n\n삭제하시겠습니까? + 아래 모든 기록 항목이 삭제됩니다\n\n[ 모든 색상 라이브러리 ]\n\n모두 삭제하시겠습니까? 삭제 하시겠습니까? 제거 하시겠습니까? 정말 제거하시겠습니까? @@ -609,6 +637,7 @@ 복사 위치 클립 보드에 복사 값을 복사하십시오 + 복사 중... 국가 코드 (XX) 국가 코드는 두 개의 대문자로 입력해야 합니다 현재 버전 @@ -635,6 +664,8 @@ 파일 삭제 폴더 삭제 라인 삭제 + 이 버전을 영구 삭제할까요? + 삭제 중... 설명 대상 세부 @@ -668,6 +699,7 @@ AutoJs6 다운로드 링크 URL 을 다운로드하십시오 다운로드 + 초안 경로 가까운 서랍 열린 서랍 편집하다 @@ -701,8 +733,6 @@ 확장성 실패한 건축에 실패했습니다 - 복사하지 못했습니다 - 만들지 못했습니다 서명 키 생성에 실패했습니다 삭제하지 못했습니다 접근성 서비스를 비활성화하지 못했습니다 @@ -726,11 +756,13 @@ 업데이트 실패 파일을 쓰지 못했습니다 파일 + 파일: %1$s 파일 세부정보 존재하는 파일입니다 파일 탐색기 파일 확장자 파일 이름 + 파일 이름: %1$s 파일이 존재하지 않는다 파일 경로 \"%1$s\" 파일은 실행 가능한 스크립트가 아닙니다 @@ -747,6 +779,7 @@ 성명 플로팅 버튼 폴더 + 폴더: %1$s 강제 정지 전경 서비스 포맷 완료 @@ -768,7 +801,8 @@ 버튼 숨기기 이 노드를 숨기기 동일 프레임의 노드를 숨기기 - 역사 + 기록 + 기록이 삭제되었습니다 배터리 최적화를 무시합니다 업데이트를 무시했습니다 @@ -796,6 +830,7 @@ IP 주소 \"%1$s\" 는 특수 용도 (loopback/broadcast/multicast/reserved/...) 를 가지므로, 대상 서버에 연결하는 데 사용할 수 없을 수도 있습니다. 새로운 버전이 발견되지 않았습니다 피드백 + 항목: %1$d *.js 파일 도약 끝까지 점프하십시오 @@ -848,8 +883,14 @@ 원본 경로와 대상 경로가 같습니다. 이동이 중지되었습니다. 모든 파일을 새 디렉토리로 이동하십시오 파일 이동 + 파일을 휴지통으로 이동 폴더 이동 + 폴더를 휴지통으로 이동 이동 위치 + 휴지통으로 이동하지 못했습니다 + 휴지통으로 이동했습니다 + 이동 중... + 휴지통으로 이동 중... 여러 옵션 이름 접근성 서비스를 활성화해야합니다 @@ -867,7 +908,8 @@ 브라우저가 설치되지 않았습니다 내용 없음 콘텐츠 설명 없음 - 역사가 없습니다 + 데이터 없음 + 기록 없음 무시되지 않았습니다 복사할 로그 항목이 없습니다 내보낼 로그 항목이 없습니다 @@ -876,6 +918,7 @@ 뿌리가 아닌 루트 액세스가 없습니다 실행을 멈출 스크립트가 없습니다 + 버전 기록 없음 부여되지 않았습니다 설치되지 않음 주요 활동을 보여주지 않습니다 @@ -901,14 +944,17 @@ 원래 개발자 기타 APK 출력 경로 + 덮어쓰기 패키지 이름 패키지 이름 (계산 중...) 비밀번호 비밀번호가 비어질 수 없다 비밀번호는 최소 %d 자 이상이어야 합니다 반죽 + 경로: %1$s 경로는 비어 있습니다 PC 서버 주소 + 사용량: %1$s 앱이 네트워크를 통해 대략적인 위치 정보에 접근하도록 허용합니다 앱이 GPS 를 통해 정확한 위치 정보에 접근하도록 허용합니다 앱이 위치 제공자의 추가 명령에 접근하도록 허용합니다 @@ -1000,9 +1046,9 @@ 입력 이름 잠시만 기다려주세요... 잠시 후 다시 시도해 주세요... - Plugin center + 플러그인 센터 플러그인 상세 - Plugins + 플러그인 포인터 위치 \"포인터 위치\"토글이 실패했습니다.\n루트 액세스가 필요합니다. 게시물 알림 @@ -1038,11 +1084,14 @@ 등록하다 등록이 성공했습니다 등록 + 릴리스 기록 릴리스 노트 원격 원격 파일이 로컬 저장소에 성공적으로 저장됨 원격 프로젝트가 로컬 저장소에 성공적으로 저장되었습니다 모든 중단 점을 제거하십시오 + 휴지통에서 제거 + 이 항목을 휴지통에서 영구 삭제할까요? 이름 바꾸기 바꾸다 모두 다 바꿔 @@ -1056,6 +1105,10 @@ 재설정은 샘플에만 해당됩니다 재시작 재시작 전략 + 복원 + 원래 경로로 복원 + 지정한 경로로 복원 + 복원 중... 결과 변경 로그를 가져오는 중... 플러그인 목록 데이터를 가져오는 중입니다... @@ -1131,7 +1184,13 @@ 정렬 최근 업데이트순 정렬 이름순 정렬 + 이름 (A-Z) 패키지 크기순 정렬 + 경로 (A-Z) + 크기 (작은순) + 크기 (큰순) + 시간 (오래된순) + 시간 (최신순) 원본 소스 코드 경로 특수 권한 @@ -1162,10 +1221,15 @@ 시간이 정한 작업 정시 작업 스케줄링 엔진 타이밍 + 선택 대기 도구 + 크기: %1$s 투명 배경 런처 아이콘은 시스템에 의해 자동으로 마스킹되거나 배경이 추가될 수 있어 장치마다 실제 효과가 다를 수 있습니다 + 휴지통 지도 시간 유형 + 파일 + 폴더 현재 버전의 AutoJs6 에 위의 ABI 가 포함되어 있지 않으므로 선택한 ABI \"%s\" 를 사용하여 패키지 응용 프로그램을 만들 수 없습니다. 사용할 수 없음 ABI \"%s\" 를 사용할 수 없습니다 @@ -1174,15 +1238,15 @@ @string/text_under_development 실행 취소 - Uninstall + 삭제 알 수 없음 알 수 없는 작성자 알 수 없는 설명 알 수 없는 제목 알 수 없는 버전 미확인 - Updatable - Update + 업데이트 가능 + 업데이트 업데이트 확인 된 상태가 지워졌습니다 나중 @@ -1210,7 +1274,10 @@ 버전 버전 코드 버전 코드 (계산 중...) - 버전 기록 + 버전: %1$s + 버전 기록 + 복원하면 편집기 내용만 변경됩니다. 파일에 자동으로 저장되지 않습니다. 저장 버튼을 수동으로 눌러 주세요. + 복원하면 파일 내용이 덮어써집니다.\n경로: %1$s 버전 정보 버전 이름 버전 이름 (계산 중...) @@ -1225,28 +1292,4 @@ 보안 설정을 작성하십시오 시스템 설정을 작성하십시오 백그라운드 팝업 - 새로고침에 실패했습니다 - 저장에 실패했습니다 - 초안 경로 - 복사 - 다른 이름으로 저장 - 버전 기록 - 버전 기록 없음 - 복원하면 편집기 내용만 변경됩니다. 파일에 자동으로 저장되지 않습니다. 저장 버튼을 수동으로 눌러 주세요. - 휴지통으로 이동 - 영구 삭제 - 휴지통으로 이동했습니다 - 삭제 방식 선택 - 휴지통 - 복원 경로 선택 - 원래 경로로 복원 - 지정한 경로로 복원 - 경로: %1$s - 파일 이름: %1$s - 선택 - 덮어쓰기 - 자동 이름 변경 - 데이터 없음 - 잘못된 경로: %1$s - 복원하면 파일 내용이 덮어써집니다.\n경로: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 271498dd..537ac3b5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,8 @@ + + @@ -97,6 +99,7 @@ \"Положение указателя\" - это отладочная функция в параметрах разработчика Android.\nПосле включения система будет отображать на экране сведения о точке(ах) касания, такие как [координаты/траектория движения/количество/размер/скорость движения/давление], что упрощает [написание/отладку/проверку] соответствующих скриптов. Разрешение \"отправка уведомлений\" позволяет AutoJs6 публиковать уведомления в системе, чтобы скрипты могли создавать и управлять пользовательскими уведомлениями в шторке уведомлений.\n\nNote: на устройствах Android 13+ без этого разрешения некоторые уведомления могут не отображаться, а также это может повлиять на запуск и стабильность служб на переднем плане. При наличии доступа к медиафайлам проекта предупреждение о безопасности при записи экрана не выдается. + Просмотреть историю выпусков AutoJs6 на GitHub и статистику по основным категориям. Стратегия перезагрузки влияет только на кнопку перезапуска в выдвижном меню главной страницы.\n\nБыстрая перезагрузка: быстро перезапускает приложение. Если перезапуск не удался или возникли непредвиденные ситуации, попробуйте переключиться на \"Запланированную перезагрузку\".\nЗапланированная перезагрузка: заранее настраивает краткосрочную задачу. После остановки приложение будет запущено по расписанию для выполнения перезапуска. Переключатель включён (по умолчанию): значения, возвращаемые методами Java типов Number/Boolean/Character, оборачиваются в объекты Java и доступны в скрипте (String исключается). typeof = \"object\", species = \"JavaObject\"; доступны методы Java, что помогает сохранять точные типовые характеристики Java и корректную резолюцию перегрузок.\n\nПереключатель выключен: перечисленные типы больше не оборачиваются и передаются напрямую как примитивы JavaScript (number/boolean/строка из одного символа). typeof соответствует соответствующему типу JavaScript, что ближе к семантике/экосистеме JavaScript. При необходимости можно явно создать Java‑обёртку через new, например new java.lang.Boolean(true).\n\nСм.: http://issues.autojs6.com/435 Если у вас есть экзотический root или аномальное состояние для root-доступа, вы можете принудительно установить root на root или non-root. @@ -109,7 +112,6 @@ Управляет базовым механизмом по расписанию запуска скриптов.\n\nAlarmManager: При выдаче разрешения \"Разрешить настройку будильников и напоминаний\" задачи выполняются более точно по времени.\nWorkManager: Дружелюбен к системе; подходит для некритичных задач, допускающих задержку.\nJobScheduler: Устаревшая реализация, сохранена для совместимости; вероятность задержек выше. Сценарии и отличия:\n\n1. AlarmManager\nПодходит для задач, чувствительных к точности времени, например [напоминания/строго по расписанию/...].\nВ Android 12+ при наличии разрешения \"Разрешить настройку будильников и напоминаний\" задачи запускаются более точно даже при выключенном экране/в ожидании.\nБез него система может понизить точность или отложить выполнение.\n\n2. WorkManager\nПодходит для задач без строгих требований к времени, например [некритичная синхронизация/очистка/статистика/...].\nПланирование учитывает [батарею/сеть/зарядку/политику сна/...], возможны задержки при выключенном экране или в ожидании.\nХотя точность не гарантируется, он силён в [надёжном завершении/повторных попытках/цепочках/дедупликации работ].\n\n3. JobScheduler\nИсторическая реализация для совместимости.\nНа новых версиях Android возможны более существенные задержки.\nНе рекомендуется, если это не необходимо ради совместимости. Предоставляет доступ к истории использования устройства и статистике, что позволяет получить более точный результат в функции currentPackage() - Просмотреть историю выпусков AutoJs6 на GitHub и статистику по основным категориям. Настройки безопасности системы, содержащие системные предпочтения, которые приложения могут читать, но не имеют права записывать.\nОни предназначены для параметров, которые пользователь должен явно изменить через пользовательский интерфейс системного приложения.\nПри наличии разрешения на безопасные системные настройки обычные приложения могут напрямую изменять безопасные настройки (например, служба доступности). Разрешение \"изменение системных настроек\" позволяет AutoJs6 изменять некоторые параметры системы, чтобы скрипты могли менять такие настройки, как [ яркость экрана / автоповорот / тайм-аут экрана ]. Отказаться @@ -120,11 +122,13 @@ @string/text_back Отменить Отменить + Выбрать Очистить OK Подключиться @string/text_continue @string/text_copy + Копировать Префикс деф. Подробности Удалить @@ -139,17 +143,20 @@ Свернуть Еще Открыть палитру + Предпросмотр Выйти + История Удалить + @string/text_restore Получить Повторная попытка Сохранить + Сохранить как Системные настройки Система Обновить Использовать по умолчанию Использовать палитру - История Просмотр файла Просмотреть ярлыки Просмотреть обновление @@ -212,7 +219,6 @@ Невозможно преобразовать %s в путь ScriptRuntime Не удается найти EditorView из дочернего представления: %s Не удается получить URL загрузки - Не удалось переименовать Не удается получить состояние батареи Используйте больше символов для пароля Эти пароли не совпадают @@ -240,6 +246,8 @@ Не удалось изменить состояние переключателя Не удалось преобразовать значение %s в Drawable + Не удалось скопировать + Не удалось создать Не удалось открыть страницу настроек Не удалось предоставить права на использование Shizuku Ошибка создания экземпляра \"%s\" @@ -247,10 +255,12 @@ Не удалось запустить менеджер Не удалось открыть системные настройки Не удалось загрузить плагины.\nПричина: %1$s. + Не удалось переместить + Не удалось переименовать Не удалось отобразить содержимое Не удалось получить список изменений + Не удалось получить историю релизов Не удалось получить примечания к выпуску - Не удалось получить историю версий Не удалось получить файл version.properties для анализа информации о версии Не удалось отозвать права на Shizuku Ошибка выполнения скрипта @@ -274,6 +284,7 @@ Неверный репозиторий GitHub: %s Неверный IP-адрес Неверные учетные данные для входа в систему + Недопустимый путь: %1$s Неверный остаток компаса: %s Неверный элемент карты селектора: { %s: %s (%s) } IP-адрес не может быть пустым @@ -318,11 +329,13 @@ Номер порта более 65535 Основной файл скрипта проекта \"%1$s\" не существует Не удается поместить значение %s в JSON + Не удалось обновить Невозможно найти предыдущее значение с помощью regex Повторяющийся символ двоеточия Повторяющийся символ точки Требуемое свойство \"%1$s\" пусто или не существует Разрешённый путь для \"%1$s\" не может быть равен null + Не удалось сохранить Скрипт завершается Метод селектора должен быть вызван, а не передан в качестве параметра: %s Невозможно использовать компас, когда тип результата - селектор @@ -334,12 +347,18 @@ Преобразователь для требуемого свойства \"%1$s\" не может возвращать пустое значение Нить не жива Тайм-аут при запросе информации о плагине %1$s. + Текущий путь к файлу неизвестен; невозможно показать сведения о файле + Текущий путь к файлу неизвестен; невозможно показать историю версий + Текущий путь к файлу неизвестен; невозможно открыть в других приложениях + Текущий путь к файлу неизвестен; невозможно упаковать приложение + Текущий путь к файлу неизвестен; невозможно сохранить файл Невозможно использовать службу Shizuku Недопустимый символ Неизвестная ошибка Неизвестный параметр селектора алгоритма: %s Неизвестное имя цвета: %s Неизвестное имя элемента: %1$s + Неизвестное исключение Неизвестная гравитация: %s Неизвестная операция Неизвестный тип результата {имя: %s} @@ -347,7 +366,8 @@ Неизвестный селектор {имя: %s, тип: %s} Неизвестный тип: %s Формат WebP-Lossless нельзя сжать с параметром качества; используйте вместо этого JPEG/PNG/WebP-Lossy - \"Файл не существует или не доступен для чтения: %s\". + Файл не существует или недоступен для чтения + \"Файл не существует или не доступен для чтения: %s\". Служба %1$s в фоновом режиме Нажмите для запуска %1$s Фоновая служба %1$s @@ -362,6 +382,8 @@ 0 для бесконечного цикла Поддерживаются IPv4, IPv6 и доменные имена. Введите URL, указывающий на удалённый плагин.\nНапример: \"https://example.com/plugin.apk\". + Удалить навсегда + Переместить в корзину Последнее использование: %1$s Поток \"blob\" неудачен Поток \"blob\" успешен, запись офлайн-кеша @@ -429,12 +451,12 @@ Разрешить приложению создавать и отправлять уведомления Выполнять перед файлом сценария Показывать сообщение при начале или прекращении записи сценария + Просмотр истории релизов и статистики Примитивы Java в скрипте будут обёрнуты в объекты Java Более стабильный анализ макета, но худшая совместимость кода (требуется перезапуск службы доступности) Добавление ярлыков в пусковую установку Запуск или остановка записи, управляемая клавишей уменьшения громкости, когда отображается плавающая кнопка Нажмите клавишу \"Громкость вверх\", чтобы остановить все запущенные скрипты - Просмотреть историю выпусков и статистические данные Внутреннее хранилище Служба доступности Требуется для автоматической работы сценария (щелчок, длительное нажатие, скольжение и т.д.). @@ -448,8 +470,8 @@ Об отображении поверх других приложений Обратная связь Лицензии + @string/text_release_history Обновить - @string/text_version_histories Доступ был предоставлен Щелчок (click) Длинный щелчок (longClick) @@ -466,8 +488,7 @@ Все Доступ ко всем файлам \"Доступ ко всем файлам\" необходим для доступа к файлам сценариев на телефоне - Вся история - Вся история очищена + Все записи истории Все элементы удалены Рекомендуется выдать разрешение \"Разрешить настройку будильников и напоминаний\", чтобы обеспечить максимально точный запуск задач даже при выключенном экране или в режиме ожидания Скопировано в буфер обмена @@ -508,8 +529,8 @@ документы AutoJs6 Журнал Журнал - AutoJs6 Plugin Center - Plugins + Центр плагинов AutoJs6 + Плагины AutoJs6 Настройки Настройки Исходный код @@ -521,6 +542,7 @@ Автоматическая проверка обновлений Автоматическая проверка обновлений и отображение закуски на домашней странице Автоматический ночной режим + Автопереименование Доступные окна Назад Действие @@ -553,17 +575,23 @@ Проверить обновления (игнорировать локальную версию) При проверке обновлений напрямую получать последнюю удалённую версию Проверка обновлений... + Выберите способ удаления + Выберите путь восстановления Город или населенный пункт Имя класса / пакета Очистить Очистить выбор файла Очистить сценарий предварительного выполнения + Очистить корзину + Очистить все элементы в корзине? Очистить состояния проверки обновлений - Click icon to add launcher shortcut + Очистить историю версий + Очистить все элементы истории версий? + Нажмите на значок, чтобы добавить ярлык на экран Нажмите на элемент для удаления Нажмите на пункт, чтобы просмотреть подробности Нажмите \"OK\" для перехода к настройкам - Click other areas to exit selection + Нажмите в другом месте, чтобы выйти из выбора Частые операции Режим клиента Клонировать цветовую библиотеку @@ -579,10 +607,10 @@ Совместимость Доверие Конфигурация - Вы уверены, что хотите очистить всю историю? Вы уверены, что хотите удалить все элементы? - Будут очищены следующие элементы истории\n\n[ %1$s ]\n\nВы уверены, что хотите очистить их? - Будут очищены следующие элементы истории\n\n[ Все цветовые библиотеки ]\n\nВы уверены, что хотите их очистить? + Очистить историю? + Следующая история будет очищена\n\n[ %1$s ]\n\nОчистить? + Все записи истории ниже будут очищены\n\n[ Все цветовые библиотеки ]\n\nОчистить все? Вы уверены, что хотите удалить? Вы уверены, что хотите удалить? Вы действительно хотите удалить? @@ -607,18 +635,19 @@ Копировать в Копировать в буфер обмена Копировать значение + Копирование... Код страны (XX) Код страны должен состоять из двух заглавных букв Текущая версия Ежедневная задача Дата Пн - Вторник - Ср. - Thu - Fri + Вт + Ср + Чт + Пт Сб - Солнце + Вс Отладка Возобновить Шаг в @@ -633,6 +662,8 @@ Удалить файл Удалить папку Удалить строку + Удалить эту ревизию навсегда? + Удаление... Описание Назначение Детали @@ -666,6 +697,7 @@ Ссылка на скачивание AutoJs6 URL-адрес загрузки Загрузка + Путь к черновику Закрыть ящик Открыть ящик Редактировать @@ -699,8 +731,6 @@ Расширяемость Не удалось Не удалось создать - Не удалось скопировать - Не удалось создать Не удалось создать ключ подписи Не удалось удалить Не удалось отключить службу доступности @@ -724,11 +754,13 @@ Не удалось обновить Не удалось записать файл Файл + Файл: %1$s Сведения о файле Файл уже существует Проводник файлов Расширения файлов Имя файла + Имя файла: %1$s Файл не существует Путь к файлу Файл \"%1$s\" не является исполняемым скриптом @@ -745,6 +777,7 @@ Полное имя Плавающая кнопка Папка + Папка: %1$s Принудительная остановка Служба переднего плана Форматирование завершено @@ -766,7 +799,8 @@ Кнопка скрытия Скрыть этот узел Скрыть узел того же фрейма - Истории + История + История очищена Значок Игнорировать оптимизацию батареи Игнорировать обновления @@ -794,6 +828,7 @@ IP-адрес \"%1$s\" имеет специальное назначение (loopback/broadcast/multicast/reserved/...), поэтому он может быть непригоден для подключения к целевому серверу. Более новая версия не найдена Обратная связь + Элементы: %1$d *.js файл Перейти Перейти к концу @@ -846,8 +881,14 @@ Исходный путь совпадает с путем назначения; перемещение отменено. Переместить все файлы в новый каталог Переместить файл + Переместить файл в корзину Переместить папку + Переместить папку в корзину Переместить в + Не удалось переместить в корзину + Перемещено в корзину + Перемещение... + Перемещение в корзину... Несколько вариантов Имя Необходимо включить службу доступности @@ -865,7 +906,8 @@ Не установлен браузер Нет содержимого Нет описания содержимого - Нет истории + Нет данных + Истории нет Нет проигнорированных обновлений Нет записей журнала для копирования Нет записей журнала для экспорта @@ -874,6 +916,7 @@ Не root Нет root-доступа Нет скриптов для остановки выполнения + Нет истории версий Не предоставляется Не установлено Не показывает основную деятельность @@ -899,14 +942,17 @@ Оригинальный разработчик Другие Путь вывода APK + Перезаписать Имя пакета Имя пакета (Вычисление...) Пароль Пароль не может быть пустым Пароль должен содержать не менее %d символов Вставить + Путь: %1$s Путь пуст Адрес сервера ПК + Использовано: %1$s Разрешить приложению получать приблизительную геолокацию через сеть Разрешить приложению получать точную геолокацию через GPS Разрешить приложению доступ к дополнительным командам провайдера геолокации @@ -998,9 +1044,9 @@ Имя ввода Пожалуйста, подождите... Пожалуйста, повторите попытку позже... - Plugin center + Центр плагинов Сведения о плагине - Plugins + Плагины Расположение указателя Переключение \"Расположение указателя\" не удалось.\nТребуется корневой доступ. почтовые уведомления @@ -1036,11 +1082,14 @@ Регистрировать Регистрация прошла успешно Регистрация + История релизов Примечания к выпуску удаленный Удаленный файл успешно сохранен в локальном хранилище Удаленный проект успешно сохранен в локальном хранилище Удалить все точки останова + Убрать из корзины + Удалить этот элемент из корзины навсегда? Переименовать Заменить Заменить все @@ -1054,6 +1103,10 @@ Сброс выполняется только для образцов Перезапустить Стратегия перезагрузки + Восстановить + Восстановить в исходный путь + Восстановить в указанный путь + Восстановление... Результат Получение списка изменений... Получение данных списка плагинов... @@ -1129,7 +1182,13 @@ Сортировать Сортировать по времени последнего обновления Сортировать по имени + Имя (A-Z) Сортировать по размеру пакета + Путь (A-Z) + Размер (сначала маленькие) + Размер (сначала большие) + Время (сначала старые) + Время (сначала новые) Источник Путь к исходному коду Специальные разрешения @@ -1160,10 +1219,15 @@ Задание по времени Движок планирования таймерных задач Время + Выбрать Инструменты + Размер: %1$s Значок запуска с прозрачным фоном может быть автоматически замаскирован системой или получить фон, поэтому фактический результат может отличаться на разных устройствах + Корзина Учебник Тип + Файл + Папка Выбранный ABI \"%s\" не может быть использован для создания упакованного приложения, поскольку текущая версия AutoJs6 не включает указанный ABI. Недоступно ABI \"%s\" недоступен @@ -1172,15 +1236,15 @@ @string/text_under_development Отозвать Верни - Uninstall + Удалить Неизвестно Неизвестный Автор Неизвестное Описание Неизвестный Заголовок Неизвестная Версия Не подтверждено - Updatable - Update + Доступно обновление + Обновить Обновления Обновления проверенные состояния очищены Позже @@ -1208,7 +1272,10 @@ Версия Код версии Код версии (Вычисление...) - История версий + Версии: %1$s + История версий + Восстановление изменяет только содержимое редактора. Файл не будет сохранен автоматически. Нажмите кнопку \"Сохранить\" вручную. + Восстановление перезапишет содержимое файла.\nПуть: %1$s Информация о версии Название версии Название версии (Вычисление...) @@ -1223,28 +1290,4 @@ Параметры безопасности записи Запись системных настроек Всплывающие окна в фоне - Не удалось обновить - Не удалось сохранить - Путь к черновику - Копировать - Сохранить как - История версий - Нет истории версий - Восстановление изменяет только содержимое редактора. Файл не будет сохранен автоматически. Нажмите кнопку \"Сохранить\" вручную. - Переместить в корзину - Удалить навсегда - Перемещено в корзину - Выберите способ удаления - Корзина - Выберите путь восстановления - Восстановить в исходный путь - Восстановить в указанный путь - Путь: %1$s - Имя файла: %1$s - Выбрать - Перезаписать - Автопереименование - Нет данных - Недопустимый путь: %1$s - Восстановление перезапишет содержимое файла.\nПуть: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 11ac22cc..efeb93c2 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -95,6 +95,7 @@ \"指針位置\" 是安卓開發者選項中的調試功能.\n開啓後, 系統會在屏幕上顯示觸摸點的 [座標/移動軌跡/數量/大小/移動速度/壓力] 等信息, 便於相關腳本的 [編寫/調試/校對] 等. \"發送通知\" 權限允許 AutoJs6 向系統發佈通知, 使腳本可以在通知欄發佈並管理自定義通知等.\n\n注: 在 Android 13+ 設備上未授予該權限時, 部分通知可能無法顯示, 並可能影響前台服務的啓動與穩定性. 被授予 \"投影媒體權限\" 後, 錄製屏幕的安全提示窗口將不再彈出. + 查看 AutoJs6 在 GitHub 發行版本的歷史更新記錄及重要分類的統計數據. 重啓策略僅作用於主頁抽屜欄的重啓按鈕.\n\n快速重啓: 快速重啓應用, 如果重啓失敗或出現非預期情況, 可嘗試切換至 \"計劃重啓\".\n計劃重啓: 提前設置一個短時定時任務, 應用停止後會再次定時啓動, 以實現應用重啓. 開關啓用 (默認): 將 Java 方法返回的 Number/Boolean/Character 包裝為 Java 對象暴露到腳本 (String 除外). typeof 為 \"object\", species 為 \"JavaObject\", 可訪問這些對象的 Java 方法, 利於保留 Java 精確類型特徵與方法重載行為.\n\n開關禁用: 不再包裝上述類型, 直接作為 JavaScript 基元值 (number/boolean/單字符字符串). typeof 為相應 JavaScript 類型, 更貼近 JavaScript 語義與生態. 但仍可通過 new 關鍵字顯式聲明一個 Java 包裝類型, 如 new java.lang.Boolean(true).\n\n參閲: http://issues.autojs6.com/435 如果設備使用非常規 Root 方式或 Root 權限檢測結果異常, 可設置 \"強制 Root 模式\" 或 \"強制非 Root 模式\". @@ -107,7 +108,6 @@ 控制腳本定時觸發的底層機制.\n\nAlarmManager: 通過申請 \"允許設置鬧鐘和提醒\" 權限, 可使定時任務的觸發更接近準時.\nWorkManager: 系統友好, 適合可接受一定延遲的非關鍵定時任務.\nJobScheduler: 舊實現, 僅用作兼容保留, 定時任務出現延遲的概率較大. 使用場景與差異説明: \n\n1. AlarmManager\n適用於準時性敏感的定時任務, 如 [提醒/嚴格定時腳本/...].\n在 Android 12+ 設備上獲取 \"允許設置鬧鐘和提醒\" 權限後, 在息屏或待機狀態時能夠較準時地執行定時任務.\n在未授予 \"允許設置鬧鐘和提醒\" 權限時, 系統可能降級為不準時或延後執行任務.\n\n2. WorkManager\n適用於不要求嚴格準時執行的定時任務, 如 [非關鍵性同步/清理/統計/...].\n根據系統 [ 電量/網絡/充電情況/待機策略/... ] 進行調度, 息屏或待機狀態時可能推遲任務執行.\n雖無法保證時間精確性, 但在 [ 可靠完成/失敗重試/鏈式執行/唯一任務去重 ] 等方面擁有獨特優勢.\n\n3. Job\n歷史實現, 為兼容保留.\n在高版本安卓系統中定時任務可能存在較大的延遲執行問題.\n除非出於兼容需要, 一般不推薦使用. \"查看使用情況\" 權限可獲取本設備過去使用應用的情況, 從而使 \"獲取當前應用\" (currentPackage) 結果更準確 - 查看 AutoJs6 在 GitHub 發行版本的歷史更新記錄及重要分類的統計數據. 安全設置包含應用程序可讀但不可寫入的設置選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設置權限\" 後, 普通應用可直接修改上述安全設置 (例如無障礙服務). \"修改系統設置\" 權限允許 AutoJs6 修改部分系統設置項, 使腳本可以修改 [ 屏幕亮度 / 自動旋轉 / 屏幕超時 ] 等系統設置參數. 放棄 @@ -118,11 +118,13 @@ @string/text_back 取消 取消下載 + 選擇路徑 清空記錄 確定 連接 @string/text_continue @string/text_copy + 複製路徑 默認前綴 瞭解更多 關閉 @@ -137,17 +139,20 @@ 最小化 瞭解更多 打開調色盤 + 預覽 放棄 + 發行歷史 移除 + @string/text_restore 獲取 重試 保存 + 另存為 系統設置 @string/dialog_button_system_settings 立即更新 使用默認值 使用調色盤 - 版本歷史 查看文件 查看快捷方式 查看更新 @@ -210,7 +215,6 @@ 無法將 %s 轉換為腳本運行時路徑 無法在子控件 %s 中找到 EditorView 無法獲取下載鏈接 - 重命名失敗 無法獲取設備電池狀態 密碼長度不足 密碼不一致 @@ -238,6 +242,8 @@ 開關狀態改變失敗 無法將值 %s 轉換為 Drawable 實例 + 複製失敗 + 創建失敗 跳轉設置頁面失敗 Shizuku 權限授予失敗 實例 \"%s\" 創建失敗 @@ -245,10 +251,12 @@ 管理器啓動失敗 系統設置啓動失敗 插件讀取失敗.\n原因: %1$s. + 移動失敗 + 重命名失敗 內容渲染失敗 獲取更新日誌失敗 + 獲取發行歷史失敗 獲取發行説明失敗 - 獲取版本歷史失敗 獲取 version.properties 失敗, 無法解析版本信息 Shizuku 權限撤銷失敗 腳本運行失敗 @@ -272,6 +280,7 @@ 無效的 GitHub 倉庫: %s 無效的 IP 地址 無效的登錄憑證 + 無效的路徑: %1$s 無效的剩餘羅盤參數: %s 不合法的選擇器集合元素: { %s: %s (%s) } IP 地址不可為空 @@ -316,11 +325,13 @@ 端口號超過 65535 項目主腳本文件 \"%1$s\" 不存在 無法將值 %s 存入 JSON + 刷新失敗 正則表達式不支持向前查找 重複的分號符號 重複的句點符號 必需屬性 \"%1$s\" 為空或不存在 解析後的路徑 \"%1$s\" 不可為 null + 保存失敗 腳本正在退出中 選擇器方法需調用而不可作為參數直接傳入: %s 結果類型為 selector 時, 無法同時使用羅盤參數 @@ -332,12 +343,18 @@ 必需屬性 \"%1$s\" 的轉換器不能返回空值 線程處於非活動狀態 查詢 %1$s 插件信息超時. + 當前文件路徑未知, 無法顯示文件詳情 + 當前文件路徑未知, 無法顯示版本歷史 + 當前文件路徑未知, 無法使用其他應用打開 + 當前文件路徑未知, 無法打包應用 + 當前文件路徑未知, 無法保存文件 無法使用 Shizuku 服務 不接受的字符 未知錯誤 未知的 algorithm 選擇器參數: %s 未知顏色名稱: %s 未知的元素名稱: %1$s + 未知異常 未知的重力參數: %s 未知操作指令 未知結果類型 {名稱: %s} @@ -345,7 +362,8 @@ 未知選擇器 {名稱: %s, 類型: %s} 未知類型: %s WebP-Lossless 格式無法使用 quality 質量參數進行壓縮, 可改用 JPEG/PNG/WebP-Lossy - 文件不存在或不可讀: %s + 文件不存在或不可讀 + 文件不存在或不可讀: %s %1$s 前台服務 點擊返回 %1$s 主界面 %1$s 前台服務 @@ -360,6 +378,8 @@ 0 表示無限循環 支持 IPv4, IPv6 及域名. 輸入一個指向遠程插件地址的 URL.\n例如 \"https://example.com/plugin.apk\". + 永久刪除 + 移入回收站 最近使用: %1$s 線程 \"blob\" 請求失敗 線程 \"blob\" 請求成功, 寫入離線緩存 @@ -427,12 +447,12 @@ 允許應用創建併發送通知 在腳本文件之前執行 錄製開始及停止時顯示提示消息 + 查看發行版本歷史更新記錄與統計數據 Java 原始類型在腳本中將被包裝為對應的 Java 對象 增強佈局分析穩定性但降低代碼兼容性 (需重啓無障礙服務) 添加快捷方式到啓動器 按 \"音量減\" 鍵開始或停止腳本錄製 (需開啓浮動按鈕) 按 \"音量加\" 鍵停止所有正在運行的腳本 - 查看發行版本歷史更新記錄與統計數據 內部存儲 無障礙服務 腳本自動操作 (點擊/長按/滑動等) 所需 @@ -446,8 +466,8 @@ 關於顯示在其他應用上層 問題反饋 許可信息 + @string/text_release_history 檢查更新 - @string/text_version_histories 授權成功 點擊 (click) 長按 (longClick) @@ -464,8 +484,7 @@ 全部 所有文件管理權限 需要授予 \"所有文件管理權限\" 才能正常讀寫腳本文件 - 全部歷史記錄 - 歷史記錄已清空 + 全部歷史記錄 全部記錄已清空 建議授予 \"允許設置鬧鐘和提醒\" 權限以確保定時任務在息屏或待機狀態時儘可能準時執行 已複製到剪貼板 @@ -506,8 +525,8 @@ 文檔 AutoJs6 日誌 日誌 - AutoJs6 Plugin Center - Plugins + AutoJs6 插件中心 + 插件 AutoJs6 設置 設置 軟件源碼 @@ -519,6 +538,7 @@ 自動檢查更新 自動檢查更新並在首頁下方顯示通知條 自動夜間模式 + 自動重命名 可用窗口 返回 動作 (Action) @@ -551,17 +571,23 @@ 檢查更新 (忽略本地版本) 檢查更新時, 直接從遠程獲取最新版本 正在檢查更新... + 選擇刪除策略 + 選擇恢復路徑 城市或區域 類/包名 清空 清空文件選擇 清空預執行腳本 + 清空回收站 + 是否清空回收站全部條目 清除更新檢查狀態 - Click icon to add launcher shortcut + 清空全部歷史記錄 + 是否清空版本歷史全部條目 + 點擊圖標添加啓動器快捷方式 點擊條目可移除 點擊條目可查看詳情 點擊 \"確定\" 跳轉到設置頁面 - Click other areas to exit selection + 點擊其他區域退出選擇 操作頻率過快 客户端模式 克隆顏色庫 @@ -577,10 +603,10 @@ 兼容性 置信度 配置 - 是否確定清空歷史記錄 是否確定清空全部記錄 - 以下歷史記錄將被清空\n\n[ %1$s ]\n\n是否確定清空 - 以下歷史記錄將被清空\n\n[ 全部顏色庫 ]\n\n是否確定清空 + 是否確定清空歷史記錄 + 以下歷史記錄將被清空\n\n[ %1$s ]\n\n是否確定清空 + 以下歷史記錄將被清空\n\n[ 全部顏色庫 ]\n\n是否確定清空 是否確定刪除 是否確定移除 是否確定卸載 @@ -605,6 +631,7 @@ 複製到 複製到剪貼板 複製值 + 複製中... 國家代碼 (XX) 國家代碼必須為兩個大寫字母 當前版本 @@ -631,6 +658,8 @@ 刪除文件 刪除文件夾 刪除行 + 是否永久刪除此版本記錄 + 正在刪除... 描述 目標 詳情 @@ -664,6 +693,7 @@ AutoJs6 下載鏈接 下載地址 下載中 + 草稿文件路徑 關閉側拉抽屜 打開側拉抽屜 編輯 @@ -697,8 +727,6 @@ 擴展性 失敗 打包失敗 - 複製失敗 - 創建失敗 生成簽名密鑰失敗 刪除失敗 禁用無障礙服務失敗 @@ -722,11 +750,13 @@ 更新失敗 文件寫入失敗 文件 + 文件: %1$s 文件詳情 文件已存在 文件管理器 文件擴展名 文件名 + 文件名: %1$s 文件不存在 文件路徑 文件 \"%1$s\" 非可執行腳本 @@ -743,6 +773,7 @@ 姓名 浮動按鈕 文件夾 + 文件夾: %1$s 強制停止 前台服務 格式化已完成 @@ -764,7 +795,8 @@ 隱藏按鈕 隱藏此節點 隱藏同框節點 - 歷史記錄 + 歷史記錄 + 歷史記錄已清空 圖標 忽略電池優化 已忽略更新 @@ -792,6 +824,7 @@ IP 地址 \"%1$s\" 具備特殊用途 (迴環/廣播/多播/保留/...), 因此可能無法用於連接目標服務器. 已是最新版本 問題反饋 + 項目: %1$d *.js 文件 跳轉 跳轉到文件末尾 @@ -844,8 +877,14 @@ 源路徑與目標路徑相同, 移動操作中止 移動原目錄文件到新目錄 移動文件 + 將文件移入回收站 移動文件夾 + 將文件夾移入回收站 移動到 + 移入回收站失敗 + 已移入回收站 + 移動中... + 正在移入回收站... 多個可選項 名稱 需要啓用無障礙服務 @@ -863,7 +902,8 @@ 未安裝瀏覽器應用 無內容 無內容描述 - 無歷史記錄 + 無數據 + 無歷史記錄 暫無已忽略更新 無可複製的日誌條目 無可導出的日誌條目 @@ -872,6 +912,7 @@ 免 root 無 root 權限 無運行中的腳本 + 無版本歷史 未授予 未安裝 不顯示主界面 @@ -897,14 +938,17 @@ 原始開發者 其他 打包應用保存路徑 + 覆蓋 包名 包名 (計算中...) 密碼 密碼不能為空 密碼至少需要 %d 位字符 粘貼 + 路徑: %1$s 路徑為空 PC 服務端地址 + 使用率: %1$s 允許應用通過網絡訪問粗略的位置信息 允許應用通過 GPS 獲取精確的位置信息 允許應用訪問位置提供者的額外命令 @@ -996,9 +1040,9 @@ 請輸入名稱 請稍候... 請稍後再嘗試此操作... - Plugin center + 插件中心 插件詳情 - Plugins + 插件 指針位置 切換 \"指針位置\" 顯示狀態失敗\n可能缺少 root 權限 發佈通知權限 @@ -1034,11 +1078,14 @@ 註冊 註冊成功 註冊中 + 發行歷史 發行説明 遠程 遠程文件已保存至本地 遠程項目已保存至本地 刪除所有斷點 + 從回收站移除 + 是否永久刪除此項目 重命名 替換 全部替換 @@ -1052,6 +1099,10 @@ 只能重置示例代碼文件 重啓 重啓策略 + 恢復 + 恢復到原始路徑 + 恢復到指定路徑 + 正在恢復... 結果 正在獲取更新日誌... 正在獲取插件列表數據... @@ -1127,7 +1178,13 @@ 排序 按最近更新排序 按名稱排序 + 名稱 (A-Z) 按安裝包大小排序 + 路徑 (A-Z) + 大小 (最小優先) + 大小 (最大優先) + 時間 (最舊優先) + 時間 (最新優先) 來源 源代碼路徑 特殊權限 @@ -1158,10 +1215,15 @@ 定時任務 定時任務調度引擎 定時 + 待選擇 工具 + 大小: %1$s 透明背景啓動器圖標可能被系統自動添加遮罩或背景, 因此不同設備的實際效果與預期可能不完全一致 + 回收站 教程 類型 + 文件 + 文件夾 選擇的 ABI 架構 \"%s\" 無法用於創建打包應用, 因當前 AutoJs6 版本未包含上述 ABI 架構. 不可用 ABI \"%s\" 不可用 @@ -1170,15 +1232,15 @@ @string/text_under_development 撤銷 撤銷 - Uninstall + 卸載 未知 未知開發者 未知描述 未知標題 未知版本 未驗證 - Updatable - Update + 可更新 + 更新 更新 更新檢查狀態已清除 稍後提示 @@ -1206,7 +1268,10 @@ 版本 版本號 版本號 (計算中...) - 版本歷史 + 版本: %1$s + 版本歷史 + 恢復時只修改編輯器內容, 不會自動保存到文件, 需要手動點擊保存按鈕. + 恢復將覆蓋文件內容.\n路徑: %1$s 版本信息 版本名稱 版本名稱 (計算中...) @@ -1221,28 +1286,4 @@ 修改安全設置 修改系統設置 後台彈出界面 - 刷新失敗 - 保存失敗 - 草稿文件路徑 - 複製路徑 - 另存為 - 版本歷史 - 無版本歷史 - 恢復時只修改編輯器內容, 不會自動保存到文件, 需要手動點擊保存按鈕. - 移入回收站 - 永久刪除 - 已移入回收站 - 選擇刪除策略 - 回收站 - 選擇恢復路徑 - 恢復到原始路徑 - 恢復到指定路徑 - 路徑: %1$s - 文件名: %1$s - 選擇路徑 - 覆蓋 - 自動重命名 - 無數據 - 無效的路徑: %1$s - 恢復將覆蓋文件內容.\n路徑: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index c1b44489..76dd9c4a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -95,6 +95,7 @@ \"指標位置\" 是安卓開發者選項中的除錯功能.\n開啟後, 系統會在螢幕上顯示觸控點的 [座標/移動軌跡/數量/大小/移動速度/壓力] 等資訊, 便於相關指令碼的 [編寫/除錯/校對] 等. \"傳送通知\" 許可權允許 AutoJs6 向系統釋出通知, 使指令碼可以在通知欄釋出並管理自定義通知等.\n\n注: 在 Android 13+ 裝置上未授予該許可權時, 部分通知可能無法顯示, 並可能影響前臺服務的啟動與穩定性. 被授予 \"投影媒體許可權\" 後, 錄製螢幕的安全提示視窗將不再彈出. + 檢視 AutoJs6 在 GitHub 發行版本的歷史更新記錄及重要分類的統計資料. 重啟策略僅作用於主頁抽屜欄的重啟按鈕.\n\n快速重啟: 快速重啟應用, 如果重啟失敗或出現非預期情況, 可嘗試切換至 \"計劃重啟\".\n計劃重啟: 提前設定一個短時定時任務, 應用停止後會再次定時啟動, 以實現應用重啟. 開關啟用 (預設): 將 Java 方法返回的 Number/Boolean/Character 包裝為 Java 物件暴露到指令碼 (String 除外). typeof 為 \"object\", species 為 \"JavaObject\", 可訪問這些物件的 Java 方法, 利於保留 Java 精確型別特徵與方法過載行為.\n\n開關禁用: 不再包裝上述型別, 直接作為 JavaScript 基元值 (number/boolean/單字元字串). typeof 為相應 JavaScript 型別, 更貼近 JavaScript 語義與生態. 但仍可透過 new 關鍵字顯式宣告一個 Java 包裝型別, 如 new java.lang.Boolean(true).\n\n參閱: http://issues.autojs6.com/435 如果裝置使用非常規 Root 方式或 Root 許可權檢測結果異常, 可設定 \"強制 Root 模式\" 或 \"強制非 Root 模式\". @@ -107,7 +108,6 @@ 控制指令碼定時觸發的底層機制.\n\nAlarmManager: 透過申請 \"允許設定鬧鐘和提醒\" 許可權, 可使定時任務的觸發更接近準時.\nWorkManager: 系統友好, 適合可接受一定延遲的非關鍵定時任務.\nJobScheduler: 舊實現, 僅用作相容保留, 定時任務出現延遲的機率較大. 使用場景與差異說明: \n\n1. AlarmManager\n適用於準時性敏感的定時任務, 如 [提醒/嚴格定時指令碼/...].\n在 Android 12+ 裝置上獲取 \"允許設定鬧鐘和提醒\" 許可權後, 在息屏或待機狀態時能夠較準時地執行定時任務.\n在未授予 \"允許設定鬧鐘和提醒\" 許可權時, 系統可能降級為不準時或延後執行任務.\n\n2. WorkManager\n適用於不要求嚴格準時執行的定時任務, 如 [非關鍵性同步/清理/統計/...].\n根據系統 [ 電量/網路/充電情況/待機策略/... ] 進行排程, 息屏或待機狀態時可能推遲任務執行.\n雖無法保證時間精確性, 但在 [ 可靠完成/失敗重試/鏈式執行/唯一任務去重 ] 等方面擁有獨特優勢.\n\n3. Job\n歷史實現, 為相容保留.\n在高版本安卓系統中定時任務可能存在較大的延遲執行問題.\n除非出於相容需要, 一般不推薦使用. \"檢視使用情況\" 許可權可獲取本裝置過去使用應用的情況, 從而使 \"獲取當前應用\" (currentPackage) 結果更準確 - 檢視 AutoJs6 在 GitHub 發行版本的歷史更新記錄及重要分類的統計資料. 安全設定包含應用程式可讀但不可寫入的設定選項, 這些選項只能由 UI 或系統級別應用修改.\n被授予 \"修改安全設定許可權\" 後, 普通應用可直接修改上述安全設定 (例如無障礙服務). \"修改系統設定\" 許可權允許 AutoJs6 修改部分系統設定項, 使指令碼可以修改 [ 螢幕亮度 / 自動旋轉 / 螢幕超時 ] 等系統設定引數. 放棄 @@ -118,11 +118,13 @@ @string/text_back 取消 取消下載 + 選擇路徑 清空記錄 確定 連線 @string/text_continue @string/text_copy + 複製路徑 預設字首 瞭解更多 關閉 @@ -137,17 +139,20 @@ 最小化 瞭解更多 開啟調色盤 + 預覽 放棄 + 發行歷史 移除 + @string/text_restore 獲取 重試 儲存 + 另存為 系統設定 @string/dialog_button_system_settings 立即更新 使用預設值 使用調色盤 - 版本歷史 檢視檔案 檢視快捷方式 檢視更新 @@ -210,7 +215,6 @@ 無法將 %s 轉換為指令碼執行時路徑 無法在子控制元件 %s 中找到 EditorView 無法獲取下載連結 - 重新命名失敗 無法獲取裝置電池狀態 密碼長度不足 密碼不一致 @@ -238,6 +242,8 @@ 開關狀態改變失敗 無法將值 %s 轉換為 Drawable 例項 + 複製失敗 + 建立失敗 跳轉設定頁面失敗 Shizuku 許可權授予失敗 例項 \"%s\" 建立失敗 @@ -245,10 +251,12 @@ 管理器啟動失敗 系統設定啟動失敗 外掛讀取失敗.\n原因: %1$s. + 移動失敗 + 重新命名失敗 內容渲染失敗 獲取更新日誌失敗 + 獲取發行歷史失敗 獲取發行說明失敗 - 獲取版本歷史失敗 獲取 version.properties 失敗, 無法解析版本資訊 Shizuku 許可權撤銷失敗 指令碼執行失敗 @@ -272,6 +280,7 @@ 無效的 GitHub 倉庫: %s 無效的 IP 地址 無效的登入憑證 + 無效的路徑: %1$s 無效的剩餘羅盤引數: %s 不合法的選擇器集合元素: { %s: %s (%s) } IP 地址不可為空 @@ -316,11 +325,13 @@ 埠號超過 65535 專案主指令碼檔案 \"%1$s\" 不存在 無法將值 %s 存入 JSON + 刷新失敗 正則表示式不支援向前查詢 重複的分號符號 重複的句點符號 必需屬性 \"%1$s\" 為空或不存在 解析後的路徑 \"%1$s\" 不可為 null + 儲存失敗 指令碼正在退出中 選擇器方法需呼叫而不可作為引數直接傳入: %s 結果型別為 selector 時, 無法同時使用羅盤引數 @@ -332,12 +343,18 @@ 必需屬性 \"%1$s\" 的轉換器不能返回空值 執行緒處於非活動狀態 查詢 %1$s 外掛資訊超時. + 當前檔案路徑未知, 無法顯示檔案詳情 + 當前檔案路徑未知, 無法顯示版本歷史 + 當前檔案路徑未知, 無法使用其他應用開啟 + 當前檔案路徑未知, 無法打包應用 + 當前檔案路徑未知, 無法儲存檔案 無法使用 Shizuku 服務 不接受的字元 未知錯誤 未知的 algorithm 選擇器引數: %s 未知顏色名稱: %s 未知的元素名稱: %1$s + 未知異常 未知的重力引數: %s 未知操作指令 未知結果型別 {名稱: %s} @@ -345,7 +362,8 @@ 未知選擇器 {名稱: %s, 型別: %s} 未知型別: %s WebP-Lossless 格式無法使用 quality 質量引數進行壓縮, 可改用 JPEG/PNG/WebP-Lossy - 檔案不存在或不可讀: %s + 檔案不存在或不可讀 + 檔案不存在或不可讀: %s %1$s 前臺服務 點選返回 %1$s 主介面 %1$s 前臺服務 @@ -360,6 +378,8 @@ 0 表示無限迴圈 支援 IPv4, IPv6 及域名. 輸入一個指向遠端外掛地址的 URL.\n例如 \"https://example.com/plugin.apk\". + 永久刪除 + 移入回收站 最近使用: %1$s 執行緒 \"blob\" 請求失敗 執行緒 \"blob\" 請求成功, 寫入離線快取 @@ -427,12 +447,12 @@ 允許應用建立併發送通知 在指令碼檔案之前執行 錄製開始及停止時顯示提示訊息 + 檢視發行版本歷史更新記錄與統計資料 Java 原始型別在指令碼中將被包裝為對應的 Java 物件 增強佈局分析穩定性但降低程式碼相容性 (需重啟無障礙服務) 新增快捷方式到啟動器 按 \"音量減\" 鍵開始或停止指令碼錄製 (需開啟浮動按鈕) 按 \"音量加\" 鍵停止所有正在執行的指令碼 - 檢視發行版本歷史更新記錄與統計資料 內部儲存 無障礙服務 指令碼自動操作 (點選/長按/滑動等) 所需 @@ -446,8 +466,8 @@ 關於顯示在其他應用上層 問題反饋 許可資訊 + @string/text_release_history 檢查更新 - @string/text_version_histories 授權成功 點選 (click) 長按 (longClick) @@ -464,8 +484,7 @@ 全部 所有檔案管理許可權 需要授予 \"所有檔案管理許可權\" 才能正常讀寫指令碼檔案 - 全部歷史記錄 - 歷史記錄已清空 + 全部歷史記錄 全部記錄已清空 建議授予 \"允許設定鬧鐘和提醒\" 許可權以確保定時任務在息屏或待機狀態時儘可能準時執行 已複製到剪貼簿 @@ -506,8 +525,8 @@ 文件 AutoJs6 日誌 日誌 - AutoJs6 Plugin Center - Plugins + AutoJs6 外掛中心 + 外掛 AutoJs6 設定 設定 軟體原始碼 @@ -519,6 +538,7 @@ 自動檢查更新 自動檢查更新並在首頁下方顯示通知條 自動夜間模式 + 自動重新命名 可用視窗 返回 動作 (Action) @@ -551,17 +571,23 @@ 檢查更新 (忽略本地版本) 檢查更新時, 直接從遠端獲取最新版本 正在檢查更新... + 選擇刪除策略 + 選擇恢復路徑 城市或區域 類/包名 清空 清空檔案選擇 清空預執行指令碼 + 清空回收站 + 是否清空回收站全部條目 清除更新檢查狀態 - Click icon to add launcher shortcut + 清空全部歷史記錄 + 是否清空版本歷史全部條目 + 點選圖示新增啟動器快捷方式 點選條目可移除 點選條目可檢視詳情 點選 \"確定\" 跳轉到設定頁面 - Click other areas to exit selection + 點選其他區域退出選擇 操作頻率過快 客戶端模式 克隆顏色庫 @@ -577,10 +603,10 @@ 相容性 置信度 配置 - 是否確定清空歷史記錄 是否確定清空全部記錄 - 以下歷史記錄將被清空\n\n[ %1$s ]\n\n是否確定清空 - 以下歷史記錄將被清空\n\n[ 全部顏色庫 ]\n\n是否確定清空 + 是否確定清空歷史記錄 + 以下歷史記錄將被清空\n\n[ %1$s ]\n\n是否確定清空 + 以下歷史記錄將被清空\n\n[ 全部顏色庫 ]\n\n是否確定清空 是否確定刪除 是否確定移除 是否確定解除安裝 @@ -605,6 +631,7 @@ 複製到 複製到剪貼簿 複製值 + 複製中... 國家代碼 (XX) 國家代碼必須是兩個大寫字母 當前版本 @@ -631,6 +658,8 @@ 刪除檔案 刪除資料夾 刪除行 + 是否永久刪除此版本記錄 + 正在刪除... 描述 目標 詳情 @@ -664,6 +693,7 @@ AutoJs6 下載連結 下載地址 下載中 + 草稿檔案路徑 關閉側拉抽屜 開啟側拉抽屜 編輯 @@ -697,8 +727,6 @@ 擴充套件性 失敗 打包失敗 - 複製失敗 - 建立失敗 生成簽名密鑰失敗 刪除失敗 禁用無障礙服務失敗 @@ -722,11 +750,13 @@ 更新失敗 檔案寫入失敗 檔案 + 檔案: %1$s 檔案詳情 檔案已存在 檔案管理器 副檔名 檔名 + 檔名: %1$s 檔案不存在 檔案路徑 檔案 \"%1$s\" 非可執行指令碼 @@ -743,6 +773,7 @@ 姓名 浮動按鈕 資料夾 + 資料夾: %1$s 強制停止 前臺服務 格式化已完成 @@ -764,7 +795,8 @@ 隱藏按鈕 隱藏此節點 隱藏同框節點 - 歷史記錄 + 歷史記錄 + 歷史記錄已清空 圖示 忽略電池最佳化 已忽略更新 @@ -792,6 +824,7 @@ IP 地址 \"%1$s\" 具備特殊用途 (迴環/廣播/多播/保留/...), 因此可能無法用於連線目標伺服器. 已是最新版本 問題反饋 + 專案: %1$d *.js 檔案 跳轉 跳轉到檔案末尾 @@ -844,8 +877,14 @@ 源路徑與目標路徑相同, 移動操作中止 移動原目錄檔案到新目錄 移動檔案 + 將檔案移入回收站 移動資料夾 + 將資料夾移入回收站 移動到 + 移入回收站失敗 + 已移入回收站 + 移動中... + 正在移入回收站... 多個可選項 名稱 需要啟用無障礙服務 @@ -863,7 +902,8 @@ 未安裝瀏覽器應用 無內容 無內容描述 - 無歷史記錄 + 無資料 + 無歷史記錄 暫無已忽略更新 無可複製的日誌條目 無可導出的日誌條目 @@ -872,6 +912,7 @@ 免 root 無 root 許可權 無執行中的指令碼 + 無版本歷史 未授予 未安裝 不顯示主介面 @@ -897,14 +938,17 @@ 原始開發者 其他 打包應用儲存路徑 + 覆蓋 包名 包名 (計算中...) 密碼 密碼不能為空 密碼至少需要 %d 個字元 貼上 + 路徑: %1$s 路徑為空 PC 服務端地址 + 使用率: %1$s 允許應用通過網絡訪問粗略的位置信息 允許應用通過 GPS 獲取精確的位置信息 允許應用訪問位置提供者的額外命令 @@ -996,9 +1040,9 @@ 請輸入名稱 請稍候... 請稍後再嘗試此操作... - Plugin center + 外掛中心 外掛詳情 - Plugins + 外掛 指標位置 切換 \"指標位置\" 顯示狀態失敗\n可能缺少 root 許可權 釋出通知許可權 @@ -1034,11 +1078,14 @@ 註冊 註冊成功 註冊中 + 發行歷史 發行說明 遠端 遠端檔案已儲存至本地 遠端專案已儲存至本地 刪除所有斷點 + 從回收站移除 + 是否永久刪除此專案 重新命名 替換 全部替換 @@ -1052,6 +1099,10 @@ 只能重置示例程式碼檔案 重啟 重啟策略 + 恢復 + 恢復到原始路徑 + 恢復到指定路徑 + 正在恢復... 結果 正在獲取更新日誌... 正在獲取外掛列表資料... @@ -1127,7 +1178,13 @@ 排序 按最近更新排序 按名稱排序 + 名稱 (A-Z) 按安裝包大小排序 + 路徑 (A-Z) + 大小 (最小優先) + 大小 (最大優先) + 時間 (最舊優先) + 時間 (最新優先) 來源 原始碼路徑 特殊許可權 @@ -1158,10 +1215,15 @@ 定時任務 定時任務排程引擎 定時 + 待選擇 工具 + 大小: %1$s 透明背景啟動器圖示可能被系統自動新增遮罩或背景, 因此不同裝置的實際效果與預期可能不完全一致 + 回收站 教程 型別 + 檔案 + 資料夾 選擇的 ABI 架構 \"%s\" 無法用於建立打包應用, 因當前 AutoJs6 版本未包含上述 ABI 架構. 不可用 ABI \"%s\" 不可用 @@ -1170,15 +1232,15 @@ @string/text_under_development 撤銷 撤銷 - Uninstall + 解除安裝 未知 未知開發者 未知描述 未知標題 未知版本 未驗證 - Updatable - Update + 可更新 + 更新 更新 更新檢查狀態已清除 稍後提示 @@ -1206,7 +1268,10 @@ 版本 版本號 版本號 (計算中...) - 版本歷史 + 版本: %1$s + 版本歷史 + 恢復時只修改編輯器內容, 不會自動儲存到檔案, 需要手動點選儲存按鈕. + 恢復將覆蓋檔案內容.\n路徑: %1$s 版本資訊 版本名稱 版本名稱 (計算中...) @@ -1221,28 +1286,4 @@ 修改安全設定 修改系統設定 後臺彈出介面 - 刷新失敗 - 儲存失敗 - 草稿檔案路徑 - 複製路徑 - 另存為 - 版本歷史 - 無版本歷史 - 恢復時只修改編輯器內容, 不會自動儲存到檔案, 需要手動點選儲存按鈕. - 移入回收站 - 永久刪除 - 已移入回收站 - 選擇刪除策略 - 回收站 - 選擇恢復路徑 - 恢復到原始路徑 - 恢復到指定路徑 - 路徑: %1$s - 檔名: %1$s - 選擇路徑 - 覆蓋 - 自動重新命名 - 無資料 - 無效的路徑: %1$s - 恢復將覆蓋檔案內容.\n路徑: %1$s \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 679e522b..c0733c34 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -95,6 +95,7 @@ \"指针位置\" 是安卓开发者选项中的调试功能.\n开启后, 系统会在屏幕上显示触摸点的 [坐标/移动轨迹/数量/大小/移动速度/压力] 等信息, 便于相关脚本的 [编写/调试/校对] 等. \"发送通知\" 权限允许 AutoJs6 向系统发布通知, 使脚本可以在通知栏发布并管理自定义通知等.\n\n注: 在 Android 13+ 设备上未授予该权限时, 部分通知可能无法显示, 并可能影响前台服务的启动与稳定性. 被授予 \"投影媒体权限\" 后, 录制屏幕的安全提示窗口将不再弹出. + 查看 AutoJs6 在 GitHub 发行版本的历史更新记录及重要分类的统计数据. 重启策略仅作用于主页抽屉栏的重启按钮.\n\n快速重启: 快速重启应用, 如果重启失败或出现非预期情况, 可尝试切换至 \"计划重启\".\n计划重启: 提前设置一个短时定时任务, 应用停止后会再次定时启动, 以实现应用重启. 开关启用 (默认): 将 Java 方法返回的 Number/Boolean/Character 包装为 Java 对象暴露到脚本 (String 除外). typeof 为 \"object\", species 为 \"JavaObject\", 可访问这些对象的 Java 方法, 利于保留 Java 精确类型特征与方法重载行为.\n\n开关禁用: 不再包装上述类型, 直接作为 JavaScript 基元值 (number/boolean/单字符字符串). typeof 为相应 JavaScript 类型, 更贴近 JavaScript 语义与生态. 但仍可通过 new 关键字显式声明一个 Java 包装类型, 如 new java.lang.Boolean(true).\n\n参阅: http://issues.autojs6.com/435 如果设备使用非常规 Root 方式或 Root 权限检测结果异常, 可设置 \"强制 Root 模式\" 或 \"强制非 Root 模式\". @@ -107,7 +108,6 @@ 控制脚本定时触发的底层机制.\n\nAlarmManager: 通过申请 \"允许设置闹钟和提醒\" 权限, 可使定时任务的触发更接近准时.\nWorkManager: 系统友好, 适合可接受一定延迟的非关键定时任务.\nJobScheduler: 旧实现, 仅用作兼容保留, 定时任务出现延迟的概率较大. 使用场景与差异说明: \n\n1. AlarmManager\n适用于准时性敏感的定时任务, 如 [提醒/严格定时脚本/...].\n在 Android 12+ 设备上获取 \"允许设置闹钟和提醒\" 权限后, 在息屏或待机状态时能够较准时地执行定时任务.\n在未授予 \"允许设置闹钟和提醒\" 权限时, 系统可能降级为不准时或延后执行任务.\n\n2. WorkManager\n适用于不要求严格准时执行的定时任务, 如 [非关键性同步/清理/统计/...].\n根据系统 [ 电量/网络/充电情况/待机策略/... ] 进行调度, 息屏或待机状态时可能推迟任务执行.\n虽无法保证时间精确性, 但在 [ 可靠完成/失败重试/链式执行/唯一任务去重 ] 等方面拥有独特优势.\n\n3. Job\n历史实现, 为兼容保留.\n在高版本安卓系统中定时任务可能存在较大的延迟执行问题.\n除非出于兼容需要, 一般不推荐使用. \"查看使用情况\" 权限可获取本设备过去使用应用的情况, 从而使 \"获取当前应用\" (currentPackage) 结果更准确 - 查看 AutoJs6 在 GitHub 发行版本的历史更新记录及重要分类的统计数据. 安全设置包含应用程序可读但不可写入的设置选项, 这些选项只能由 UI 或系统级别应用修改.\n被授予 \"修改安全设置权限\" 后, 普通应用可直接修改上述安全设置 (例如无障碍服务). \"修改系统设置\" 权限允许 AutoJs6 修改部分系统设置项, 使脚本可以修改 [ 屏幕亮度 / 自动旋转 / 屏幕超时 ] 等系统设置参数. 放弃 @@ -118,11 +118,13 @@ @string/text_back 取消 取消下载 + 选择路径 清空记录 确定 连接 @string/text_continue @string/text_copy + 复制路径 默认前缀 了解更多 关闭 @@ -137,17 +139,20 @@ 最小化 了解更多 打开调色盘 + 预览 放弃 + 发行历史 移除 + @string/text_restore 获取 重试 保存 + 另存为 系统设置 @string/dialog_button_system_settings 立即更新 使用默认值 使用调色盘 - 版本历史 查看文件 查看快捷方式 查看更新 @@ -210,7 +215,6 @@ 无法将 %s 转换为脚本运行时路径 无法在子控件 %s 中找到 EditorView 无法获取下载链接 - 重命名失败 无法获取设备电池状态 密码长度不足 密码不一致 @@ -238,6 +242,8 @@ 开关状态改变失败 无法将值 %s 转换为 Drawable 实例 + 复制失败 + 创建失败 跳转设置页面失败 Shizuku 权限授予失败 实例 \"%s\" 创建失败 @@ -245,10 +251,12 @@ 管理器启动失败 系统设置启动失败 插件读取失败.\n原因: %1$s. + 移动失败 + 重命名失败 内容渲染失败 获取更新日志失败 + 获取发行历史失败 获取发行说明失败 - 获取版本历史失败 获取 version.properties 失败, 无法解析版本信息 Shizuku 权限撤销失败 脚本运行失败 @@ -272,6 +280,7 @@ 无效的 GitHub 仓库: %s 无效的 IP 地址 无效的登录凭证 + 无效的路径: %1$s 无效的剩余罗盘参数: %s 不合法的选择器集合元素: { %s: %s (%s) } IP 地址不可为空 @@ -316,11 +325,13 @@ 端口号超过 65535 项目主脚本文件 \"%1$s\" 不存在 无法将值 %s 存入 JSON + 刷新失败 正则表达式不支持向前查找 重复的分号符号 重复的句点符号 必需属性 \"%1$s\" 为空或不存在 解析后的路径 \"%1$s\" 不可为 null + 保存失败 脚本正在退出中 选择器方法需调用而不可作为参数直接传入: %s 结果类型为 selector 时, 无法同时使用罗盘参数 @@ -332,12 +343,18 @@ 必需属性 \"%1$s\" 的转换器不能返回空值 线程处于非活动状态 查询 %1$s 插件信息超时. + 当前文件路径未知, 无法显示文件详情 + 当前文件路径未知, 无法显示版本历史 + 当前文件路径未知, 无法使用其他应用打开 + 当前文件路径未知, 无法打包应用 + 当前文件路径未知, 无法保存文件 无法使用 Shizuku 服务 不接受的字符 未知错误 未知的 algorithm 选择器参数: %s 未知颜色名称: %s 未知的元素名称: %1$s + 未知异常 未知的重力参数: %s 未知操作指令 未知结果类型 {名称: %s} @@ -345,7 +362,8 @@ 未知选择器 {名称: %s, 类型: %s} 未知类型: %s WebP-Lossless 格式无法使用 quality 质量参数进行压缩, 可改用 JPEG/PNG/WebP-Lossy - 文件不存在或不可读: %s + 文件不存在或不可读 + 文件不存在或不可读: %s %1$s 前台服务 点击返回 %1$s 主界面 %1$s 前台服务 @@ -360,6 +378,8 @@ 0 表示无限循环 支持 IPv4, IPv6 及域名. 输入一个指向远程插件地址的 URL.\n例如 \"https://example.com/plugin.apk\". + 永久删除 + 移入回收站 最近使用: %1$s 线程 \"blob\" 请求失败 线程 \"blob\" 请求成功, 写入离线缓存 @@ -427,12 +447,12 @@ 允许应用创建并发送通知 在脚本文件之前执行 录制开始及停止时显示提示消息 + 查看发行版本历史更新记录与统计数据 Java 原始类型在脚本中将被包装为对应的 Java 对象 增强布局分析稳定性但降低代码兼容性 (需重启无障碍服务) 添加快捷方式到启动器 按 \"音量减\" 键开始或停止脚本录制 (需开启浮动按钮) 按 \"音量加\" 键停止所有正在运行的脚本 - 查看发行版本历史更新记录与统计数据 内部存储 无障碍服务 脚本自动操作 (点击/长按/滑动等) 所需 @@ -446,8 +466,8 @@ 关于显示在其他应用上层 问题反馈 许可信息 + @string/text_release_history 检查更新 - @string/text_version_histories 授权成功 点击 (click) 长按 (longClick) @@ -464,8 +484,7 @@ 全部 所有文件管理权限 需要授予 \"所有文件管理权限\" 才能正常读写脚本文件 - 全部历史记录 - 历史记录已清空 + 全部历史记录 全部记录已清空 建议授予 \"允许设置闹钟和提醒\" 权限以确保定时任务在息屏或待机状态时尽可能准时执行 已复制到剪贴板 @@ -519,6 +538,7 @@ 自动检查更新 自动检查更新并在首页下方显示通知条 自动夜间模式 + 自动重命名 可用窗口 返回 动作 (Action) @@ -551,12 +571,18 @@ 检查更新 (忽略本地版本) 检查更新时, 直接从远程获取最新版本 正在检查更新... + 选择删除策略 + 选择恢复路径 城市或区域 类/包名 清空 清空文件选择 清空预执行脚本 + 清空回收站 + 是否清空回收站全部条目 清除更新检查状态 + 清空全部历史记录 + 是否清空版本历史全部条目 点击图标添加启动器快捷方式 点击条目可移除 点击条目可查看详情 @@ -577,10 +603,10 @@ 兼容性 置信度 配置 - 是否确定清空历史记录 是否确定清空全部记录 - 以下历史记录将被清空\n\n[ %1$s ]\n\n是否确定清空 - 以下历史记录将被清空\n\n[ 全部颜色库 ]\n\n是否确定清空 + 是否确定清空历史记录 + 以下历史记录将被清空\n\n[ %1$s ]\n\n是否确定清空 + 以下历史记录将被清空\n\n[ 全部颜色库 ]\n\n是否确定清空 是否确定删除 是否确定移除 是否确定卸载 @@ -605,6 +631,7 @@ 复制到 复制到剪贴板 复制值 + 复制中... 国家代码 (XX) 国家代码必须为两个大写字母 当前版本 @@ -631,6 +658,8 @@ 删除文件 删除文件夹 删除行 + 是否永久删除此版本记录 + 正在删除... 描述 目标 详情 @@ -664,6 +693,7 @@ AutoJs6 下载链接 下载地址 下载中 + 草稿文件路径 关闭侧拉抽屉 打开侧拉抽屉 编辑 @@ -697,8 +727,6 @@ 扩展性 失败 打包失败 - 复制失败 - 创建失败 生成签名密钥失败 删除失败 禁用无障碍服务失败 @@ -722,11 +750,13 @@ 更新失败 文件写入失败 文件 + 文件: %1$s 文件详情 文件已存在 文件管理器 文件扩展名 文件名 + 文件名: %1$s 文件不存在 文件路径 文件 \"%1$s\" 非可执行脚本 @@ -743,6 +773,7 @@ 姓名 浮动按钮 文件夹 + 文件夹: %1$s 强制停止 前台服务 格式化已完成 @@ -764,7 +795,8 @@ 隐藏按钮 隐藏此节点 隐藏同框节点 - 历史记录 + 历史记录 + 历史记录已清空 图标 忽略电池优化 已忽略更新 @@ -792,6 +824,7 @@ IP 地址 \"%1$s\" 具备特殊用途 (回环/广播/多播/保留/...), 因此可能无法用于连接目标服务器. 已是最新版本 问题反馈 + 项目: %1$d *.js 文件 跳转 跳转到文件末尾 @@ -844,8 +877,14 @@ 源路径与目标路径相同, 移动操作中止 移动原目录文件到新目录 移动文件 + 将文件移入回收站 移动文件夹 + 将文件夹移入回收站 移动到 + 移入回收站失败 + 已移入回收站 + 移动中... + 正在移入回收站... 多个可选项 名称 需要启用无障碍服务 @@ -863,7 +902,8 @@ 未安装浏览器应用 无内容 无内容描述 - 无历史记录 + 无数据 + 无历史记录 暂无已忽略更新 无可复制的日志条目 无可导出的日志条目 @@ -872,6 +912,7 @@ 免 root 无 root 权限 无运行中的脚本 + 无版本历史 未授予 未安装 不显示主界面 @@ -897,14 +938,17 @@ 原始开发者 其他 打包应用保存路径 + 覆盖 包名 包名 (计算中...) 密码 密码不能为空 密码至少需要 %d 位字符 粘贴 + 路径: %1$s 路径为空 PC 服务端地址 + 使用率: %1$s 允许应用通过网络访问粗略的位置信息 允许应用通过 GPS 获取精确的位置信息 允许应用访问位置提供者的额外命令 @@ -1034,11 +1078,14 @@ 注册 注册成功 注册中 + 发行历史 发行说明 远程 远程文件已保存至本地 远程项目已保存至本地 删除所有断点 + 从回收站移除 + 是否永久删除此项目 重命名 替换 全部替换 @@ -1052,6 +1099,10 @@ 只能重置示例代码文件 重启 重启策略 + 恢复 + 恢复到原始路径 + 恢复到指定路径 + 正在恢复... 结果 正在获取更新日志... 正在获取插件列表数据... @@ -1127,7 +1178,13 @@ 排序 按最近更新排序 按名称排序 + 名称 (A-Z) 按安装包大小排序 + 路径 (A-Z) + 大小 (最小优先) + 大小 (最大优先) + 时间 (最旧优先) + 时间 (最新优先) 来源 源代码路径 特殊权限 @@ -1158,10 +1215,15 @@ 定时任务 定时任务调度引擎 定时 + 待选择 工具 + 大小: %1$s 透明背景启动器图标可能被系统自动添加遮罩或背景, 因此不同设备的实际效果与预期可能不完全一致 + 回收站 教程 类型 + 文件 + 文件夹 选择的 ABI 架构 \"%s\" 无法用于创建打包应用, 因当前 AutoJs6 版本未包含上述 ABI 架构. 不可用 ABI \"%s\" 不可用 @@ -1206,7 +1268,10 @@ 版本 版本号 版本号 (计算中...) - 版本历史 + 版本: %1$s + 版本历史 + 恢复时只修改编辑器内容, 不会自动保存到文件, 需要手动点击保存按钮. + 恢复将覆盖文件内容.\n路径: %1$s 版本信息 版本名称 版本名称 (计算中...) @@ -1221,28 +1286,4 @@ 修改安全设置 修改系统设置 后台弹出界面 - 刷新失败 - 保存失败 - 草稿文件路径 - 复制路径 - 另存为 - 版本历史 - 无版本历史 - 恢复时只修改编辑器内容, 不会自动保存到文件, 需要手动点击保存按钮. - 移入回收站 - 永久删除 - 已移入回收站 - 选择删除策略 - 回收站 - 选择恢复路径 - 恢复到原始路径 - 恢复到指定路径 - 路径: %1$s - 文件名: %1$s - 选择路径 - 覆盖 - 自动重命名 - 无数据 - 无效的路径: %1$s - 恢复将覆盖文件内容.\n路径: %1$s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9566c3ec..007d3648 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -80,6 +80,7 @@ key_$_check_for_updates_while_ignoring_the_local_version key_$_client_socket_normally_closed key_$_color_select_activity_legacy_layout + key_$_delete_permanently_also_clear_history key_$_dialog_check_display_over_other_apps key_$_dialog_foreground_svc key_$_dialog_log_entries_exported @@ -91,6 +92,8 @@ key_$_documentation_source key_$_documentation_source_local key_$_documentation_source_online + key_$_drafts_max_days + key_$_drafts_max_total_bytes key_$_editor_pinch_to_zoom_change_text_size key_$_editor_pinch_to_zoom_disable key_$_editor_pinch_to_zoom_scale_view @@ -113,6 +116,11 @@ key_$_hidden_files key_$_hidden_files_not_show key_$_hidden_files_show + key_$_history_max_days + key_$_history_max_file_size_to_track_bytes + key_$_history_max_total_bytes + key_$_history_max_total_bytes_per_file + key_$_history_max_versions key_$_ignored_updates key_$_keep_screen_on_when_in_foreground key_$_keep_screen_on_when_in_foreground_all_pages @@ -136,7 +144,7 @@ key_$_night_mode_follow_system key_$_night_mode_null key_$_not_showing_main_activity - key_$_pc_server_address_histories + key_$_pc_server_address_histories key_$_post_notifications_permission key_$_post_notifications_permission_auto_request_done key_$_post_notifications_permission_request_attempted @@ -171,13 +179,15 @@ key_$_timed_task_backend_alarm key_$_timed_task_backend_job key_$_timed_task_backend_work + key_$_trash_max_days + key_$_trash_max_total_bytes key_$_updates_checked_states_cleared key_$_use_volume_control_record key_$_use_volume_control_running - key_$_version_histories + key_$_release_history key_$_version_history_restore_does_not_auto_save_to_disk key_$_working_directory - key_$_working_directory_histories + key_$_working_directory_histories key_$_working_directory_initialized ColorInt HEX @@ -261,6 +271,10 @@ Auto ZH_CN + + + + Building... Cleaning... Packaging... @@ -353,6 +367,7 @@ \"Pointer location\" is a debugging feature in Android Developer options.\nWhen enabled, the system will display information about touch point(s) on the screen, such as [coordinates/movement trajectory/count/size/movement speed/pressure], which helps with [writing/debugging/verification] of related scripts. The \"post notifications\" permission allows AutoJs6 to publish notifications to the system, so scripts can post and manage custom notifications in the notification shade.\n\nNote: on Android 13+ devices, if this permission is not granted, some notifications may not show, and it may affect foreground service startup and stability. With project media access, security warning for screen recording will not prompt. + View the release history and key category statistics of AutoJs6 on GitHub. The restart strategy only affects the restart button in the home page drawer.\n\nQuick restart: Quickly restarts the app. If the restart fails or unexpected situations occur, try switching to \"Scheduled restart\".\nScheduled restart: Sets up a short-timed task in advance. After the app stops, it will start again on schedule to achieve app restart. Switch ON (default): results of Java methods that are instances of Number/Boolean/Character are wrapped as Java objects and exposed to scripts (String excluded). typeof is \"object\", species is \"JavaObject\"; Java methods remain accessible, which helps retain precise Java type traits and overload resolution.\n\nSwitch OFF: the above types are no longer wrapped and are exposed directly as JavaScript primitives (number/boolean/one‑character string). typeof is the corresponding JavaScript type, aligning better with JavaScript semantics and ecosystem. You can still explicitly create a Java wrapper via new, e.g. new java.lang.Boolean(true).\n\nSee: http://issues.autojs6.com/435 If you have exotic root or abnormal state for root access, you can force set root to root or non-root. @@ -365,7 +380,6 @@ Controls the underlying mechanism for triggering scripts on a schedule.\n\nAlarmManager: With the \"Allow setting alarms and reminders\" permission granted, scheduled tasks can fire closer to the exact time.\nWorkManager: System-friendly; suitable for non-critical tasks that can tolerate some delay.\nJobScheduler: Legacy implementation kept for compatibility; higher chance of delays. Use cases and differences:\n\n1. AlarmManager\nBest for time-sensitive tasks, e.g., [reminders/strictly scheduled scripts/...].\nOn Android 12+ with the \"Allow setting alarms and reminders\" permission, tasks can run more on time even when the screen is off or the device is idle.\nWithout it, the system may degrade exactness or delay execution.\n\n2. WorkManager\nBest for tasks that don’t require strict timing, e.g., [non-critical sync/cleanup/statistics/...].\nScheduling depends on [battery/network/charging/idle policies/...], so execution may be postponed when the screen is off or idle.\nWhile it can’t guarantee exact timing, it excels at [reliable completion/retries/chaining/unique work de-duplication].\n\n3. JobScheduler\nHistorical implementation for compatibility.\nOn newer Android versions, tasks may be delayed more noticeably.\nGenerally not recommended unless required for compatibility. Provides access to device usage history and statistics which results in currentPackage() a more accurate result - View the release version history and key category statistics of AutoJs6 on GitHub. Secure system settings, containing system preferences that applications can read but are not allowed to write.\nThese are for preferences that the user must explicitly modify through the UI of a system app.\nWith secure system settings permission, normal applications can directly modify the secure settings (such as accessibility service). The \"write system settings\" permission allows AutoJs6 to modify some system settings, so scripts can change system parameters such as [ screen brightness / auto-rotate / screen timeout ]. Abandon @@ -376,11 +390,13 @@ @string/text_back Cancel Cancel + Choose Clear OK Connect @string/text_continue @string/text_copy + Copy path Def prefix Details Dismiss @@ -395,17 +411,20 @@ Minimize More Palette + Preview Quit + History Remove + @string/text_restore Retrieve Retry Save + Save as System Settings System Update Use default Use palette - Histories View File View shortcuts View update @@ -471,7 +490,6 @@ Cannot convert %s into a ScriptRuntime path Cannot find EditorView from child view: %s Cannot get downloading URL - Failed to rename Cannot retrieve the battery state Use more characters for the password Those passwords did not match @@ -499,6 +517,8 @@ Failed to change the toggle state Failed to convert value %s into a Drawable + Failed to copy + Failed to create Failed to open the settings page Failed to grant Shizuku access Failed to instantiate \"%s\" @@ -506,10 +526,12 @@ Failed to launcher manager Failed to launch system settings Failed to load plugins.\nReason: %1$s. + Failed to move + Failed to rename Failed to render content Failed to retrieve changelog + Failed to retrieve the release history Failed to retrieve release notes - Failed to retrieve version histories Failed to retrieve version.properties to parse version information Failed to revoke Shizuku access Failed to run script @@ -533,6 +555,7 @@ Invalid GitHub repository: %s Invalid IP address Invalid login credentials + Invalid path: %1$s Invalid rest compass: %s Invalid selector map element: { %s: %s (%s) } IP address can not be empty @@ -577,11 +600,13 @@ Port number over 65535 Project main script file \"%1$s\" does not exist Cannot put value %s into JSON + Refresh failed Cannot find previous with regex Repeated colon symbol Repeated dot symbol Required property \"%1$s\" is nullish or does not exist Resolved path for \"%1$s\" cannot be null + Save failed Script is on exiting Selector method should be called rather than passed as a parameter: %s Cannot use a compass when result type is selector @@ -593,12 +618,18 @@ The transformer for required property \"%1$s\" cannot return nullish Thread is not alive Timeout while querying %1$s plugin info. + Current file path is unknown, unable to display file details + Current file path is unknown, unable to display version history + Current file path is unknown, unable to open with other apps + Current file path is unknown, unable to package application + Current file path is unknown, unable to save file Unable to use Shizuku service Unacceptable character Unknown error Unknown algorithm selector param: %s Unknown color name: %s Unknown element name: %1$s + Unknown exception Unknown gravity: %s Unknown operation Unknown result type {name: %s} @@ -606,7 +637,8 @@ Unknown selector {name: %s, type: %s} Unknown type: %s Format WebP-Lossless cannot be compressed with a quality parameter, use JPEG/PNG/WebP-Lossy instead - File does not exist or readable: %s + File does not exist or readable + File does not exist or readable: %s %1$s foreground service Click to launch %1$s %1$s foreground service @@ -621,6 +653,8 @@ 0 for infinite loop IPv4, IPv6 and domain are supported. Enter a URL pointing to a remote plugin address.\nFor example \"https://example.com/plugin.apk\". + Delete permanently + Move to trash Latest used: %1$s \"Blob\" thread request failed \"Blob\" thread request successful, writing offline cache @@ -688,12 +722,12 @@ Allow the application to create and send notifications Execute before the script file Show message when starting or stopping recording a script + View release history and statistics Java primitive types will be wrapped as Java objects in scripts More stable layout analysis but worse code compatibility (a11y service restarting needed) Add shortcuts to launcher Start or stop recording controlled by \"Volume Down\" key when floating button is showing Press \"Volume Up\" key to stop all running scripts - View release version history and statistics Internal Storage Accessibility service Required by the script automatic operation (click, long press, slide, etc.). @@ -707,8 +741,8 @@ About display over other apps Feedback Licenses + @string/text_release_history Update - @string/text_version_histories Access has been granted Click (click) Long click (longClick) @@ -725,8 +759,7 @@ All All files access \"All files access\" is needed to access script files on the phone - All histories - All histories have been cleared + All history entries All items have been cleared Granting the \"Allow setting alarms and reminders\" permission is recommended to help tasks run as punctually as possible even when the screen is off or the device is idle Copied to clipboard @@ -780,6 +813,7 @@ Auto check for updates Check for updates automatically and show a snackbar on homepage Auto night mode + Auto rename Available windows Back Action @@ -812,12 +846,18 @@ Check for updates (ignore the local version) Fetch the latest remote version directly when checking for updates Checking for updates... + Choose delete strategy + Choose restore path City or Locality Class / Package name Clear Clear file selection Clear pre-execute script + Clear trash + Clear all items in trash? Clear updates checked states + Clear version history + Clear all version history items? Click icon to add launcher shortcut Click the item to remove Click an item to view details @@ -838,10 +878,10 @@ Compatibility Confidence Config - Are you sure to clear all histories? Are you sure to clear all items? - All histories below will be cleared\n\n[ %1$s ]\n\nAre you sure to clear them all? - All histories below will be cleared\n\n[ All color libraries ]\n\nAre you sure to clear them all? + Are you sure to clear the history? + The history below will be cleared\n\n[ %1$s ]\n\nAre you sure to clear? + All history entries below will be cleared\n\n[ All color libraries ]\n\nAre you sure to clear them all? Are you sure to delete? Are you sure to remove? Are you sure to uninstall? @@ -866,6 +906,7 @@ Copy to Copy to clipboard Copy value + Copying... Country Code (XX) Country code must be two capital letters Current version @@ -892,6 +933,8 @@ Delete file Delete folder Delete line + Delete this revision permanently? + Deleting... Description Destination Details @@ -925,6 +968,7 @@ Download link for AutoJs6 Download URL Downloading + Draft file path Close drawer Open drawer Edit @@ -958,8 +1002,6 @@ Extensibility Failed Failed to build - Failed to copy - Failed to create Failed to create signature key Failed to delete Failed to disable accessibility service @@ -983,11 +1025,13 @@ Failed to update Failed to write file File + File: %1$s File details File already exists File explorer File extensions File name + File name: %1$s File does not exist File Path File \"%1$s\" is not an executable script @@ -1004,6 +1048,7 @@ Full Name Floating button Folder + Folder: %1$s Force stop Foreground service Formatting completed @@ -1025,7 +1070,8 @@ Hide button Hide this node Hide same frame nodes - Histories + History + The history has been cleared Icon Ignore battery optimizations Ignored updates @@ -1053,6 +1099,7 @@ IP address \"%1$s\" has a special purpose (loopback/broadcast/multicast/reserved/...), so it may not be usable for connecting to the target server. No newer version found Feedback + Items: %1$d *.js file Jump Jump to end @@ -1105,8 +1152,14 @@ Source path is the same as destination path, move aborted. Move all files to new directory Move file + Move file to trash Move folder + Move folder to trash Move to + Failed to move to trash + Moved to trash + Moving... + Moving to trash... Multiple options Name Need to enable accessibility service @@ -1124,7 +1177,8 @@ No browser installed No content No content description - No histories + No data + No history No ignored updates No log entries to copy No log entries to export @@ -1133,6 +1187,7 @@ Non-root No root access No scripts to stop running + No version history Not granted Not installed Not showing main activity @@ -1158,14 +1213,17 @@ Original developer Others APK output path + Overwrite Package name Package name (computing...) Password Password cannot be empty Password must be at least %d characters long Paste + Path: %1$s Path is empty PC server address + Usage: %1$s Allow the app to access approximate location via network Allow the app to access precise location via GPS Allow the app to access extra commands for location providers @@ -1295,11 +1353,14 @@ Register Register succeeded Registering + Release history Release notes remote Remote file saved to local storage successfully Remote project saved to local storage successfully Remove all breakpoints + Remove from trash + Remove this item from trash permanently? Rename Replace Replace all @@ -1313,6 +1374,10 @@ Reset is only for samples Restart Restart strategy + Restore + Restore to original path + Restore to specified path + Restoring... Result Retrieving changelog... Retrieving plugin list data... @@ -1388,7 +1453,13 @@ Sort Sort by last update time Sort by name + Name (A-Z) Sort by package size + Path (A-Z) + Size (smallest first) + Size (largest first) + Time (oldest first) + Time (newest first) Source Source code path Special permissions @@ -1419,10 +1490,15 @@ Timed task Timed task scheduling engine Timing + To be chosen Tools + Size: %1$s The transparent background launcher icon may be automatically masked or given a background by the system, so the actual effect may vary across different devices + Trash Tutorial Type + File + Folder The selected ABI \"%s\" cannot be used to create a packaged application, because the current version of AutoJs6 does not include the above ABI. Unavailable ABI \"%s\" is unavailable @@ -1467,7 +1543,10 @@ Version Version code Version code (computing...) - Version histories + Versions: %1$s + Version history + Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually. + Restore will overwrite file content.\nPath: %1$s Version info Version name Version name (computing...) @@ -1482,28 +1561,4 @@ Write security settings Write system settings Display pop-up windows while running in the background - Refresh failed - Save failed - Draft file path - Copy path - Save as - Version history - No version history - Restoring only changes the editor content. It will not automatically save to the file. Please tap the Save button manually. - Move to trash - Delete permanently - Moved to trash - Choose delete strategy - Trash - Choose restore path - Restore to original path - Restore to specified path - Path: %1$s - File name: %1$s - Choose - Overwrite - Auto rename - No data - Invalid path: %1$s - Restore will overwrite file content.\nPath: %1$s \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b4f68163..0bd751ae 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -123,7 +123,7 @@ @@ -218,7 +218,7 @@