├── home ├── src │ ├── mock │ │ └── mock-config.json5 │ ├── main │ │ ├── resources │ │ │ ├── base │ │ │ │ ├── profile │ │ │ │ │ ├── backup_config.json │ │ │ │ │ └── main_pages.json │ │ │ │ ├── media │ │ │ │ │ ├── background.png │ │ │ │ │ ├── foreground.png │ │ │ │ │ ├── startIcon.png │ │ │ │ │ └── layered_image.json │ │ │ │ └── element │ │ │ │ │ └── color.json │ │ │ ├── rawfile │ │ │ │ ├── fox.ico │ │ │ │ ├── cathrome │ │ │ │ │ ├── contextMenus.js │ │ │ │ │ ├── contentSettings.js │ │ │ │ │ ├── liny.js │ │ │ │ │ ├── eventListener.js │ │ │ │ │ ├── cathrome.js │ │ │ │ │ ├── commands.js │ │ │ │ │ └── i18n.js │ │ │ │ └── home.html │ │ │ ├── dark │ │ │ │ └── element │ │ │ │ │ └── color.json │ │ │ └── resfile │ │ │ │ └── what │ │ │ │ └── Paw.svg │ │ ├── ets │ │ │ ├── processes │ │ │ │ └── deveowlopers_options.ets │ │ │ ├── homebackupability │ │ │ │ └── HomeBackupAbility.ets │ │ │ ├── utils │ │ │ │ ├── environment_tools.ets │ │ │ │ ├── print_tools.ets │ │ │ │ ├── resource_tools.ets │ │ │ │ ├── clipboard_tools.ets │ │ │ │ ├── performance_tools.ets │ │ │ │ ├── html_tools.ets │ │ │ │ ├── audio_tools.ets │ │ │ │ ├── want_tools.ets │ │ │ │ ├── link_tools.ets │ │ │ │ ├── preview_tools.ets │ │ │ │ ├── any_concurrent_tools.ets │ │ │ │ ├── permission_tools.ets │ │ │ │ ├── drag_drop_tools.ets │ │ │ │ ├── color_tools.ets │ │ │ │ └── kv_store_tools.ets │ │ │ ├── components │ │ │ │ ├── layouts │ │ │ │ │ └── linysAniContainer.ets │ │ │ │ ├── toggles │ │ │ │ │ ├── linysLockToggle.ets │ │ │ │ │ └── linysSwitchWithText.ets │ │ │ │ ├── texts │ │ │ │ │ ├── linysTextSubtitleDivision.ets │ │ │ │ │ ├── linysTextArea.ets │ │ │ │ │ ├── linysSymbol.ets │ │ │ │ │ ├── linysTextTitle.ets │ │ │ │ │ ├── linysLink.ets │ │ │ │ │ └── linysText.ets │ │ │ │ ├── linysProgressInfo.ets │ │ │ │ ├── buttons │ │ │ │ │ ├── linysImageButton.ets │ │ │ │ │ ├── linysTimeoutButtonWithText.ets │ │ │ │ │ ├── linysCapsuleButtonWithText.ets │ │ │ │ │ ├── linysCapsuleButton.ets │ │ │ │ │ ├── linysWindowButton.ets │ │ │ │ │ ├── linysTimeoutButton.ets │ │ │ │ │ ├── linysConfirmDenyButtons.ets │ │ │ │ │ └── linysShowButton.ets │ │ │ │ ├── linysFileNameErrorDisplay.ets │ │ │ │ ├── linysLockSlider.ets │ │ │ │ ├── linysProgress.ets │ │ │ │ └── linysPathTree.ets │ │ │ ├── workers │ │ │ │ ├── Bookmarks_loader.ets │ │ │ │ ├── Homepage_background_loader.ets │ │ │ │ ├── History_index_saver.ets │ │ │ │ ├── History_index_loader.ets │ │ │ │ └── History_indexer.ets │ │ │ ├── homeabilitystage │ │ │ │ └── HomeAbilityStage.ets │ │ │ ├── blocks │ │ │ │ ├── contents │ │ │ │ │ ├── meowWhatsNew.ets │ │ │ │ │ ├── meowCreditsRepos.ets │ │ │ │ │ └── meowCreditsUsers.ets │ │ │ │ ├── panels │ │ │ │ │ ├── meowAnimationManager.ets │ │ │ │ │ └── meowHomepageManager.ets │ │ │ │ └── modules │ │ │ │ │ └── meowExtensionView.ets │ │ │ ├── dialogs │ │ │ │ ├── quicks │ │ │ │ │ ├── woofQuickUA.ets │ │ │ │ │ └── woofQuickSE.ets │ │ │ │ ├── woofControlFrame.ets │ │ │ │ ├── web │ │ │ │ │ ├── woofAlert.ets │ │ │ │ │ ├── woofConfirm.ets │ │ │ │ │ ├── woofHttpAuthPrompt.ets │ │ │ │ │ └── woofPrompt.ets │ │ │ │ ├── managers │ │ │ │ │ └── ManageListElement.ets │ │ │ │ ├── prompts │ │ │ │ │ ├── woofPromptOK.ets │ │ │ │ │ ├── woofWantJump.ets │ │ │ │ │ └── woofWantProtectedResources.ets │ │ │ │ └── contents │ │ │ │ │ ├── woofQR.ets │ │ │ │ │ └── woofRecentFaultLogs.ets │ │ │ ├── objects │ │ │ │ ├── CatstensionBridgeObj.ets │ │ │ │ ├── HistoryDataSource.ets │ │ │ │ └── ExtWebNode.ets │ │ │ └── hosts │ │ │ │ ├── bunch_of_user_agents.ets │ │ │ │ ├── bunch_of_search_engines.ets │ │ │ │ ├── bunch_of_defaults.ets │ │ │ │ └── bunch_of_extensions.ets │ │ └── module.json5 │ ├── test │ │ ├── List.test.ets │ │ └── LocalUnit.test.ets │ └── ohosTest │ │ ├── ets │ │ └── test │ │ │ ├── List.test.ets │ │ │ └── Ability.test.ets │ │ └── module.json5 ├── .gitignore ├── oh-package.json5 ├── hvigorfile.ts ├── obfuscation-rules.txt └── build-profile.json5 ├── .gitattributes ├── AppScope ├── resources │ └── base │ │ ├── media │ │ ├── app_icon.png │ │ ├── background.png │ │ ├── foreground.png │ │ └── layered_image.json │ │ └── element │ │ └── string.json └── app.json5 ├── .gitignore ├── oh-package.json5 ├── hvigorfile.ts ├── code-linter.json5 ├── auto_build.bat ├── auto_build_ohos.bat ├── oh-package-lock.json5 ├── LICENSE ├── hvigor └── hvigor-config.json5 └── README.md /home/src/mock/mock-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ets linguist-language=ts -------------------------------------------------------------------------------- /home/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /build 5 | /.cxx 6 | /.test -------------------------------------------------------------------------------- /home/src/main/resources/base/profile/backup_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowToBackupRestore": true 3 | } -------------------------------------------------------------------------------- /home/src/main/resources/base/profile/main_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "pages/Index" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /home/src/main/ets/processes/deveowlopers_options.ets: -------------------------------------------------------------------------------- 1 | // This is long-term temporary 2 | // So it's common to see this empty -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/fox.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/home/src/main/resources/rawfile/fox.ico -------------------------------------------------------------------------------- /home/src/test/List.test.ets: -------------------------------------------------------------------------------- 1 | import localUnitTest from './LocalUnit.test'; 2 | 3 | export default function testsuite() { 4 | localUnitTest(); 5 | } -------------------------------------------------------------------------------- /AppScope/resources/base/media/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/AppScope/resources/base/media/app_icon.png -------------------------------------------------------------------------------- /AppScope/resources/base/media/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/AppScope/resources/base/media/background.png -------------------------------------------------------------------------------- /AppScope/resources/base/media/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/AppScope/resources/base/media/foreground.png -------------------------------------------------------------------------------- /home/src/ohosTest/ets/test/List.test.ets: -------------------------------------------------------------------------------- 1 | import abilityTest from './Ability.test'; 2 | 3 | export default function testsuite() { 4 | abilityTest(); 5 | } -------------------------------------------------------------------------------- /AppScope/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "app_name", 5 | "value": "BrowserCat" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /home/src/main/resources/base/media/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/home/src/main/resources/base/media/background.png -------------------------------------------------------------------------------- /home/src/main/resources/base/media/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/home/src/main/resources/base/media/foreground.png -------------------------------------------------------------------------------- /home/src/main/resources/base/media/startIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/LinysBrowser_NEXT/HEAD/home/src/main/resources/base/media/startIcon.png -------------------------------------------------------------------------------- /AppScope/resources/base/media/layered_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "layered-image": { 3 | "background": "$media:background", 4 | "foreground": "$media:foreground" 5 | } 6 | } -------------------------------------------------------------------------------- /home/src/main/resources/base/media/layered_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "layered-image": 3 | { 4 | "background" : "$media:background", 5 | "foreground" : "$media:foreground" 6 | } 7 | } -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/contextMenus.js: -------------------------------------------------------------------------------- 1 | // Currently empty api. 2 | // TODO: Finish contextMenus.js 3 | 4 | class cathrome_contextMenus { 5 | constructor() { 6 | } 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /local.properties 4 | /.idea 5 | **/build 6 | /.hvigor 7 | .cxx 8 | /.clangd 9 | /.clang-format 10 | /.clang-tidy 11 | **/.test 12 | /.appanalyzer -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/contentSettings.js: -------------------------------------------------------------------------------- 1 | // Currently empty api. 2 | // TODO: Finish contentSettings.js 3 | 4 | class cathrome_contentSettings { 5 | constructor() { 6 | } 7 | } -------------------------------------------------------------------------------- /home/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "name": "home", 3 | "version": "1.0.0", 4 | "description": "Meow.", 5 | "main": "", 6 | "author": "", 7 | "license": "", 8 | "dependencies": {} 9 | } 10 | 11 | -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Meow 7 | 8 | 9 | -------------------------------------------------------------------------------- /oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.0", 3 | "description": "Meow.", 4 | "dependencies": { 5 | }, 6 | "devDependencies": { 7 | "@ohos/hypium": "1.0.19", 8 | "@ohos/hamock": "1.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { appTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 6 | } 7 | -------------------------------------------------------------------------------- /home/src/ohosTest/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "home_test", 4 | "type": "feature", 5 | "deviceTypes": [ 6 | "default", 7 | "tablet" 8 | ], 9 | "deliveryWithInstall": true, 10 | "installationFree": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/liny.js: -------------------------------------------------------------------------------- 1 | class cathrome_liny { 2 | constructor() { 3 | } 4 | 5 | test(data) { 6 | console.log(`[Extension][CatsBridge][js] Test! data=[${data}]! Is this the same as output from [arkTs]?`); 7 | CatsBridge.liny_test(data); 8 | } 9 | } -------------------------------------------------------------------------------- /home/src/main/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#E1E9E3" 6 | }, 7 | { 8 | "name": "block_color", 9 | "value": "#CDD7CD" 10 | }, 11 | { 12 | "name": "font_color_title", 13 | "value": "#243B24" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /home/src/main/resources/dark/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#0F1A0F" 6 | }, 7 | { 8 | "name": "block_color", 9 | "value": "#2A392A" 10 | }, 11 | { 12 | "name": "font_color_title", 13 | "value": "#E1E9E3" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /AppScope/app.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "bundleName": "meow.liny.browser.cat.uwu", 4 | "vendor": "Linys Workshop of Cats and Catzardry", 5 | "versionCode": 1000028, 6 | "versionName": "1.7.10", 7 | "icon": "$media:layered_image", 8 | "label": "$string:app_name", 9 | "multiAppMode": { 10 | "multiAppModeType": "appClone", 11 | "maxCount": 5 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /code-linter.json5: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "**/*.ets" 4 | ], 5 | "ignore": [ 6 | "**/src/ohosTest/**/*", 7 | "**/src/test/**/*", 8 | "**/src/mock/**/*", 9 | "**/node_modules/**/*", 10 | "**/oh_modules/**/*", 11 | "**/build/**/*", 12 | "**/.preview/**/*" 13 | ], 14 | "ruleSet": [ 15 | "plugin:@performance/recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "rules": { 19 | } 20 | } -------------------------------------------------------------------------------- /home/src/main/ets/homebackupability/HomeBackupAbility.ets: -------------------------------------------------------------------------------- 1 | import { hilog } from '@kit.PerformanceAnalysisKit'; 2 | import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; 3 | 4 | export default class HomeBackupAbility extends BackupExtensionAbility { 5 | async onBackup() { 6 | hilog.info(0x0000, 'testTag', 'onBackup ok'); 7 | } 8 | 9 | async onRestore(bundleVersion: BundleVersion) { 10 | hilog.info(0x0000, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); 11 | } 12 | } -------------------------------------------------------------------------------- /home/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { hapTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins: [], /* Custom plugin to extend the functionality of Hvigor. */ 6 | config: { 7 | ohos: { 8 | overrides: { 9 | buildOption: { 10 | arkOptions: { 11 | buildProfileFields: { 12 | linysBuildTime: (new Date().toLocaleString()), 13 | } 14 | } 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /home/src/main/ets/utils/environment_tools.ets: -------------------------------------------------------------------------------- 1 | import { display } from '@kit.ArkUI'; 2 | 3 | /** 4 | * Retrieves the context from AppStorage (from abilityStage). 5 | * @returns Context. 6 | * */ 7 | export function meowContext() { 8 | return AppStorage.get('context') as Context; 9 | } 10 | 11 | /** 12 | * Converts px to vp. 13 | * @param px The true px. 14 | * @returns The equivalent vp. 15 | * */ 16 | export function meowPx2vp(px: number) { 17 | try { 18 | return px / display.getDefaultDisplaySync().densityPixels; 19 | } catch (e) { 20 | // This is not good. 21 | console.error('[px2vp] Error: ' + e); 22 | return px; 23 | } 24 | } -------------------------------------------------------------------------------- /auto_build.bat: -------------------------------------------------------------------------------- 1 | "C:\Program Files\Huawei\DevEco Studio\tools\node\node.exe" "C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.js" --sync -p product=default --analyze=normal --parallel --incremental --daemon 2 | "C:\Program Files\Huawei\DevEco Studio\tools\node\node.exe" "C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.js" --mode module -p product=default assembleHap --analyze=normal --parallel --incremental --daemon 3 | 4 | set PROJECT_PATH="E:\linys\Harmony_Projects\Linys_Browser_NEXT\" 5 | 6 | echo F| xcopy %PROJECT_PATH%"home\build\default\outputs\default\home-default-unsigned.hap" %PROJECT_PATH%"build_auto\HMOS-home-default-unsigned.hap" /Y -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/eventListener.js: -------------------------------------------------------------------------------- 1 | // Helper to manage event listeners (similar to chrome.events.Event) 2 | class cathrome_eventListener { 3 | constructor() { 4 | /** @private */ 5 | this.listeners = []; 6 | } 7 | 8 | /** @param {function} callback */ 9 | addListener(callback) { 10 | this.listeners.push(callback); 11 | } 12 | 13 | /** @param {function} callback */ 14 | removeListener(callback) { 15 | this.listeners = this.listeners.filter(l => l !== callback); 16 | } 17 | 18 | /** @param {...any} args */ 19 | fire(...args) { 20 | this.listeners.forEach(listener => listener(...args)); 21 | } 22 | } -------------------------------------------------------------------------------- /home/src/main/ets/components/layouts/linysAniContainer.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from "../../hosts/bunch_of_defaults"; 2 | 3 | // @Reusable // Is this ok? Nope, animation would be lost 4 | @Component 5 | struct linysAniContainer { 6 | // Contents 7 | @BuilderParam contents: () => void; 8 | // UI 9 | @State vis: Visibility = Visibility.None; 10 | 11 | build() { 12 | Column({ space: 12 }) { 13 | this.contents(); 14 | } 15 | .visibility(this.vis) 16 | .animation(animation_default()) 17 | .alignItems(HorizontalAlign.Start) 18 | .justifyContent(FlexAlign.Start) 19 | .onAppear(() => { 20 | this.vis = Visibility.Visible; 21 | }) 22 | } 23 | } 24 | 25 | export default linysAniContainer; -------------------------------------------------------------------------------- /auto_build_ohos.bat: -------------------------------------------------------------------------------- 1 | "D:\Program Files\Huawei\DevEco Studio\tools\node\node.exe" "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.js" --sync -p product=ohos --analyze=normal --parallel --incremental --daemon 2 | "D:\Program Files\Huawei\DevEco Studio\tools\node\node.exe" "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.js" --mode module -p product=ohos assembleHap --analyze=normal --parallel --incremental --daemon 3 | 4 | set PROJECT_PATH="E:\linys\Harmony_Projects\Linys_Browser_NEXT\" 5 | 6 | echo F| xcopy %PROJECT_PATH%"home\build\ohos\outputs\default\home-default-unsigned.hap" %PROJECT_PATH%"build_auto\OHOS-home-default-unsigned.hap" /Y 7 | echo F| xcopy %PROJECT_PATH%"home\build\ohos\outputs\default\home-default-signed.hap" %PROJECT_PATH%"build_auto\entry.hap" /Y 8 | -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/cathrome.js: -------------------------------------------------------------------------------- 1 | class cathrome { 2 | constructor() { 3 | // Chrome apis 4 | this.alarms = new cathrome_alarms(); 5 | this.action = new cathrome_action(); 6 | this.bookmarks = new cathrome_bookmarks(); 7 | this.browsingData = new cathrome_browsingData(); 8 | this.commands = new cathrome_commands(); 9 | this.contentSettings = new cathrome_contentSettings(); 10 | this.contextMenus = new cathrome_contextMenus(); 11 | this.declarativeNetRequest = new cathrome_declarativeNetRequest(); 12 | this.i18n = new cathrome_i18n(); 13 | 14 | // BrowserCat stuffs 15 | this.liny = new cathrome_liny(); 16 | this.liny.test('New cathrome constructed! uwu_!!!'); 17 | } 18 | } 19 | 20 | chrome = new cathrome(); -------------------------------------------------------------------------------- /home/src/main/ets/utils/print_tools.ets: -------------------------------------------------------------------------------- 1 | import { print } from '@kit.BasicServicesKit'; 2 | 3 | /** 4 | * Print web! 5 | * @param controller The webview controller. 6 | * */ 7 | export function print_web(controller: WebviewController, context: Context) { 8 | let print_adapter: print.PrintDocumentAdapter | undefined = AppStorage.get('print_adapter') as print.PrintDocumentAdapter | undefined; 9 | if (print_adapter == undefined) { 10 | let jobName = 'webPrint' + Date.now().toString(); 11 | print_adapter = controller.createWebPrintDocumentAdapter(jobName); 12 | print.print(jobName, print_adapter, null, context).then(() => { 13 | print_adapter = undefined; 14 | AppStorage.set('print_adapter', undefined); 15 | }); 16 | } else { 17 | console.log('[Meow][meowWebView] Cannot print! Another print job going on?'); 18 | } 19 | } -------------------------------------------------------------------------------- /home/src/main/ets/utils/resource_tools.ets: -------------------------------------------------------------------------------- 1 | import { meowContext } from './environment_tools'; 2 | 3 | /** 4 | * Analyses a ResourceStr and converts it to a string object. 5 | * @param resourceStr A Resource, indicating the identifier of the ResourceStr. 6 | * @returns A string, the string content retrieved. 7 | * @example resource_to_string($r('app.string.Whats_new_content_1')) 8 | * */ 9 | export function resource_to_string(resourceStr: Resource, context?: Context) { 10 | try { 11 | if (context) { 12 | return context.resourceManager.getStringSync(resourceStr.id); 13 | } 14 | return meowContext().resourceManager.getStringSync(resourceStr.id); 15 | // TODO: replace deprecated getStringSync when api20 is eventually released 16 | } catch (error) { 17 | console.error('[resource_to_string] ' + error); 18 | return 'meow Error!'; 19 | } 20 | } -------------------------------------------------------------------------------- /home/src/main/ets/utils/clipboard_tools.ets: -------------------------------------------------------------------------------- 1 | import lazy { BusinessError, pasteboard } from '@kit.BasicServicesKit'; 2 | 3 | export function copy(content: string) { 4 | let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); 5 | let pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, content); 6 | 7 | systemPasteboard.setData(pasteData).then(() => { 8 | console.info('Succeeded in setting PasteData. Copied \"' + content + "\""); 9 | }).catch((err: BusinessError) => { 10 | console.error('Failed to set PasteData. Cause: ' + err.message); 11 | }); 12 | } 13 | 14 | // export async function read_first_of_board() { 15 | // let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); 16 | // let text: string = (await systemPasteboard.getData()).getPrimaryText(); 17 | // return text; 18 | // } -------------------------------------------------------------------------------- /home/src/main/ets/utils/performance_tools.ets: -------------------------------------------------------------------------------- 1 | import lazy { hidebug } from '@kit.PerformanceAnalysisKit'; 2 | 3 | @Concurrent 4 | export function getPss_sync(): bigint { 5 | return hidebug.getPss(); 6 | } 7 | 8 | @Concurrent 9 | export function getVss_sync(): bigint { 10 | return hidebug.getVss(); 11 | } 12 | 13 | @Concurrent 14 | export function getCPU_sync(): number { 15 | return hidebug.getCpuUsage(); 16 | } 17 | 18 | @Concurrent 19 | export function getSharedDirty_sync(): bigint { 20 | return hidebug.getSharedDirty(); 21 | } 22 | 23 | @Concurrent 24 | export function getPrivateDirty_sync(): bigint { 25 | return hidebug.getPrivateDirty(); 26 | } 27 | 28 | @Concurrent 29 | export function getNativeHeapSize_sync(): bigint { 30 | return hidebug.getNativeHeapSize(); 31 | } 32 | 33 | @Concurrent 34 | export function getGraphicsMemory_sync(): number { 35 | return hidebug.getGraphicsMemorySync(); 36 | } 37 | -------------------------------------------------------------------------------- /home/src/main/ets/components/toggles/linysLockToggle.ets: -------------------------------------------------------------------------------- 1 | import { fontSize_Large } from '../../hosts/bunch_of_defaults'; 2 | 3 | @Component 4 | struct linysLockToggle { 5 | @Link locked: boolean; 6 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 7 | @State color: ResourceColor | undefined = undefined; 8 | // animation 9 | @State triggerValueReplace: number = 0; 10 | 11 | build() { 12 | SymbolGlyph(this.locked ? $r('sys.symbol.lock') : $r('sys.symbol.lock_open')) 13 | .onClick(() => { 14 | this.locked = !this.locked; 15 | this.triggerValueReplace += 1; 16 | })// .clickEffect(click_effect_default()) 17 | .fontSize(fontSize_Large()) 18 | .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), this.triggerValueReplace) 19 | .fontColor([this.color ? this.color : this.color_current_font]) 20 | } 21 | } 22 | 23 | export default linysLockToggle -------------------------------------------------------------------------------- /home/src/main/ets/components/texts/linysTextSubtitleDivision.ets: -------------------------------------------------------------------------------- 1 | import { fontSize_Large } from '../../hosts/bunch_of_defaults'; 2 | 3 | @Component 4 | struct linysTextSubtitleDivision { 5 | // Colors 6 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 7 | @Prop icon: ResourceStr = ''; 8 | @Prop text: ResourceStr = $r('app.string.HomeAbility_label'); 9 | @Prop color: ResourceColor = ""; 10 | @Prop max_lines: number = 1; 11 | opa: number = 0.6; 12 | 13 | build() { 14 | Row({ space: 8 }) { 15 | if (this.icon) { 16 | Text(this.icon) 17 | .fontColor(this.color == "" ? this.color_current_font : this.color) 18 | .fontSize(fontSize_Large() + 4) 19 | .fontWeight(FontWeight.Medium) 20 | } 21 | Text(this.text) 22 | .fontColor(this.color == "" ? this.color_current_font : this.color) 23 | .fontSize(fontSize_Large()) 24 | .fontWeight(FontWeight.Bold) 25 | } 26 | .opacity(this.opa) 27 | } 28 | } 29 | 30 | export default linysTextSubtitleDivision; 31 | -------------------------------------------------------------------------------- /oh-package-lock.json5: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "stableOrder": true, 4 | "enableUnifiedLockfile": false 5 | }, 6 | "lockfileVersion": 3, 7 | "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", 8 | "specifiers": { 9 | "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", 10 | "@ohos/hypium@1.0.19": "@ohos/hypium@1.0.19" 11 | }, 12 | "packages": { 13 | "@ohos/hamock@1.0.0": { 14 | "name": "@ohos/hamock", 15 | "version": "1.0.0", 16 | "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", 17 | "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", 18 | "registryType": "ohpm" 19 | }, 20 | "@ohos/hypium@1.0.19": { 21 | "name": "@ohos/hypium", 22 | "version": "1.0.19", 23 | "integrity": "sha512-cEjDgLFCm3cWZDeRXk7agBUkPqjWxUo6AQeiu0gEkb3J8ESqlduQLSIXeo3cCsm8U/asL7iKjF85ZyOuufAGSQ==", 24 | "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.19.har", 25 | "registryType": "ohpm" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /home/src/main/ets/utils/html_tools.ets: -------------------------------------------------------------------------------- 1 | let chars: string[] = 2 | ["\"", "\'", "&", "<", ">", String.fromCharCode(8194), String.fromCharCode(8195), String.fromCharCode(160), 3 | "©", "®", "™", "×", "÷"] 4 | let codes: string[] = 5 | [""", "'", "&", "<", ">", " ", " ", " ", 6 | "©", "®", "™", "×", "÷"] 7 | 8 | export function encode_string_to_html_code(input: string) { 9 | let result = ""; 10 | for (let index = 0; index < input.length; index++) { 11 | // Check each character 12 | let char = input.substring(index, index + 1); 13 | if (chars.includes(char)) { 14 | // Encode special chars 15 | result += codes[chars.indexOf(char)]; 16 | } else { 17 | // No action for else chars 18 | result += char; 19 | } 20 | } 21 | return result; 22 | } 23 | 24 | export function decode_html_code_to_string(input: string) { 25 | let result = input; 26 | for (let index = 0; index < codes.length; index++) { 27 | result = result.replaceAll(codes[index], chars[index]); 28 | } 29 | return result; 30 | } -------------------------------------------------------------------------------- /home/obfuscation-rules.txt: -------------------------------------------------------------------------------- 1 | # Define project specific obfuscation rules here. 2 | # You can include the obfuscation configuration files in the current module's build-profile.json5. 3 | # 4 | # For more details, see 5 | # https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 6 | 7 | # Obfuscation options: 8 | # -disable-obfuscation: disable all obfuscations 9 | # -enable-property-obfuscation: obfuscate the property names 10 | # -enable-toplevel-obfuscation: obfuscate the names in the global scope 11 | # -compact: remove unnecessary blank spaces and all line feeds 12 | # -remove-log: remove all console.* statements 13 | # -print-namecache: print the name cache that contains the mapping from the old names to new names 14 | # -apply-namecache: reuse the given cache file 15 | 16 | # Keep options: 17 | # -keep-property-name: specifies property names that you want to keep 18 | # -keep-global-name: specifies names that you want to keep in the global scope 19 | 20 | -enable-property-obfuscation 21 | -enable-toplevel-obfuscation 22 | -enable-filename-obfuscation 23 | -enable-export-obfuscation -------------------------------------------------------------------------------- /home/src/main/ets/utils/audio_tools.ets: -------------------------------------------------------------------------------- 1 | import lazy { audio } from '@kit.AudioKit'; 2 | import lazy { BusinessError } from '@kit.BasicServicesKit'; 3 | 4 | export function activate_mix_audio_session() { 5 | let audioManager = audio.getAudioManager(); 6 | let audioSessionManager: audio.AudioSessionManager = audioManager.getSessionManager(); 7 | 8 | let strategy: audio.AudioSessionStrategy = { 9 | concurrencyMode: audio.AudioConcurrencyMode.CONCURRENCY_MIX_WITH_OTHERS 10 | }; 11 | 12 | try { 13 | audioSessionManager.setAudioSessionScene(audio.AudioSessionScene.AUDIO_SESSION_SCENE_MEDIA); 14 | } catch (error) { 15 | console.error(`[activate_mix_audio_session] Failed: ${error}`); 16 | } 17 | 18 | audioSessionManager.activateAudioSession(strategy).then(() => { 19 | console.info('[activate_mix_audio_session] Succeeded in doing activateAudioSession! CONCURRENCY_MIX_WITH_OTHERS!'); 20 | }).catch((err: BusinessError) => { 21 | console.error(`[activate_mix_audio_session] Failed to activateAudioSession (CONCURRENCY_MIX_WITH_OTHERS). Code: ${err.code}, message: ${err.message}`); 22 | }); 23 | } -------------------------------------------------------------------------------- /home/src/main/ets/components/linysProgressInfo.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, click_effect_default } from '../hosts/bunch_of_defaults'; 2 | import linysText from './texts/linysText'; 3 | 4 | @Component 5 | struct linysProgressInfo { 6 | @Prop progress: string = "qwq"; 7 | @Prop notification: ResourceStr = "The task desc."; 8 | @State is_hov: boolean = false; 9 | 10 | build() { 11 | Row({ space: 10 }) { 12 | linysText({ text: this.notification, max_lines: 3 }) 13 | .layoutWeight(1) 14 | linysText({ text: this.progress }) 15 | } // Reindexing indicator 16 | .padding({ 17 | left: 15, 18 | right: 15, 19 | top: 8, 20 | bottom: 8 21 | }) 22 | .brightness(this.is_hov ? 1.1 : 1) 23 | .backgroundColor($r('sys.color.comp_background_tertiary')) 24 | .animation(animation_default()) 25 | .alignItems(VerticalAlign.Center) 26 | .borderRadius(12) 27 | .clickEffect(click_effect_default()) 28 | .constraintSize({ maxWidth: 500 }) 29 | .onHover((ish) => { 30 | this.is_hov = ish; 31 | }) 32 | } 33 | } 34 | 35 | export default linysProgressInfo; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 awa_Liny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /home/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "apiType": "stageMode", 3 | "buildOption": { 4 | "externalNativeOptions": { 5 | "abiFilters": [ 6 | "arm64-v8a", 7 | "x86_64" 8 | ] 9 | }, 10 | "arkOptions": { 11 | "buildProfileFields": { 12 | "linysBuildTime": "0", 13 | } 14 | }, 15 | "sourceOption": { 16 | "workers": [ 17 | './src/main/ets/workers/History_indexer.ets', 18 | './src/main/ets/workers/History_index_loader.ets', 19 | './src/main/ets/workers/History_index_saver.ets', 20 | './src/main/ets/workers/Bookmarks_loader.ets', 21 | './src/main/ets/workers/Homepage_background_loader.ets' 22 | ] 23 | }, 24 | }, 25 | "buildOptionSet": [ 26 | { 27 | "name": "release", 28 | "arkOptions": { 29 | "obfuscation": { 30 | "ruleOptions": { 31 | "enable": false, 32 | "files": [ 33 | "./obfuscation-rules.txt" 34 | ] 35 | } 36 | } 37 | } 38 | }, 39 | ], 40 | "targets": [ 41 | { 42 | "name": "default" 43 | }, 44 | { 45 | "name": "ohosTest", 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /home/src/main/ets/utils/want_tools.ets: -------------------------------------------------------------------------------- 1 | // Wants 2 | 3 | /** 4 | * Respond to want. 5 | * @param uri the want uri 6 | * @param action the want action 7 | * @param type the want type 8 | * @returns True if a want is detected and processed. 9 | * */ 10 | export function check_want(uri: string, action: string, type: string, storage: LocalStorage) { 11 | if (uri.length == 0) { 12 | console.log('[Meow][init] Didn\'t load want for an empty want.'); 13 | return false; 14 | } 15 | console.log('[Meow][want] Load want with uri: ' + uri + ', action: ' + action + ', type: ' + type + ', storage id: ' + storage.get('my_window_id')); 16 | 17 | // Respond to want 18 | if (uri.substring(0, 7) == 'file://') { 19 | want_file(uri, storage); 20 | } else { 21 | if (action == 'ohos.want.action.viewData') { 22 | want_web(uri, storage); 23 | } 24 | } 25 | uri = ""; 26 | return true; 27 | } 28 | 29 | function want_file(uri: string, storage: LocalStorage) { 30 | console.log('[Meow][Want] Want uri is [' + uri + '].'); 31 | storage.set('universal_new_tab_gateway', [uri, false]); 32 | } 33 | 34 | function want_web(uri: string, storage: LocalStorage) { 35 | console.log('[Meow][want] Load want uri: ' + uri); 36 | storage.set('universal_new_tab_gateway', [uri, false]); 37 | } -------------------------------------------------------------------------------- /home/src/main/ets/components/texts/linysTextArea.ets: -------------------------------------------------------------------------------- 1 | @Component 2 | struct linysTextArea { 3 | @Link text: string; 4 | init_text: string = ''; 5 | max_lines: number | undefined = undefined; 6 | // Colors 7 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 8 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 9 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 10 | // Actions 11 | onSubmitAction?: (enterKey: EnterKeyType) => void; 12 | 13 | build() { 14 | TextArea({ text: this.init_text }) // linysText({ text: this.link, max_lines: 5 })// link 15 | .maxLines(this.max_lines) 16 | .fontWeight(FontWeight.Regular) 17 | .fontColor(this.color_current_font) 18 | .caretColor(this.color_current_font) 19 | .selectedBackgroundColor(this.color_current_font) 20 | .width("100%") 21 | .padding(10) 22 | .onSubmit((enterKey) => { 23 | if (this.onSubmitAction) { 24 | this.onSubmitAction(enterKey); 25 | } 26 | }) 27 | .onChange((v) => { 28 | this.text = v; 29 | }) 30 | } 31 | } 32 | 33 | export default linysTextArea; -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysImageButton.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, click_effect_default, fontSize_Icon_Button } from '../../hosts/bunch_of_defaults'; 2 | import { path_2_uri } from '../../utils/storage_tools'; 3 | 4 | @Component 5 | struct linysImageButton { 6 | @Prop path: string; 7 | // Settings 8 | can_hover: boolean = false; 9 | radius: number = 10; 10 | // UI 11 | @State isHov: boolean = false; 12 | // Actions 13 | execution?: () => void; 14 | 15 | // aboutToAppear(): void { 16 | // console.log(`[linysImageButton] path=${this.path}`); 17 | // } 18 | 19 | build() { 20 | Image(this.path ? path_2_uri(this.path) : '') 21 | .brightness(this.isHov ? 1.1 : 1) 22 | .animation(animation_default()) 23 | .height(fontSize_Icon_Button() + (this.can_hover ? 10 : 0)) 24 | .width(fontSize_Icon_Button() + (this.can_hover ? 10 : 0)) 25 | .padding(this.can_hover ? 5 : 0) 26 | .borderRadius(this.can_hover ? this.radius : 0) 27 | .clickEffect(click_effect_default()) 28 | .onHover((isHv) => { 29 | if (this.can_hover) { 30 | this.isHov = isHv; 31 | } 32 | }) 33 | .onClick(() => { 34 | if (this.execution) { 35 | this.execution(); 36 | } 37 | }) 38 | } 39 | } 40 | 41 | export default linysImageButton; -------------------------------------------------------------------------------- /home/src/main/ets/workers/Bookmarks_loader.ets: -------------------------------------------------------------------------------- 1 | import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; 2 | import { bunch_of_bookmarks } from '../hosts/bunch_of_bookmarks'; 3 | 4 | const workerPort: ThreadWorkerGlobalScope = worker.workerPort; 5 | 6 | /** 7 | * Defines the event handler to be called when the worker thread receives a message sent by the host thread. 8 | * The event handler is executed in the worker thread. 9 | * 10 | * @param event message data 11 | */ 12 | workerPort.onmessage = async (event: MessageEvents) => { 13 | let html_bookmarks = event.data as string; 14 | let bookmarks = new bunch_of_bookmarks('Bookmarks~Meow'); 15 | bookmarks.import_html(html_bookmarks, true); 16 | bookmarks.init = true; 17 | workerPort.postMessageWithSharedSendable(bookmarks); 18 | }; 19 | 20 | /** 21 | * Defines the event handler to be called when the worker receives a message that cannot be deserialized. 22 | * The event handler is executed in the worker thread. 23 | * 24 | * @param event message data 25 | */ 26 | workerPort.onmessageerror = (event: MessageEvents) => { 27 | }; 28 | 29 | /** 30 | * Defines the event handler to be called when an exception occurs during worker execution. 31 | * The event handler is executed in the worker thread. 32 | * 33 | * @param event error message 34 | */ 35 | workerPort.onerror = (event: ErrorEvent) => { 36 | }; -------------------------------------------------------------------------------- /hvigor/hvigor-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.0", 3 | "dependencies": { 4 | }, 5 | "execution": { 6 | // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ 7 | // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ 8 | // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ 9 | // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ 10 | // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ 11 | }, 12 | "logging": { 13 | // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ 14 | }, 15 | "debugging": { 16 | // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ 17 | }, 18 | "nodeOptions": { 19 | // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ 20 | // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysTimeoutButtonWithText.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import linysText from '../texts/linysText'; 3 | import linysTimeoutButton from './linysTimeoutButton'; 4 | 5 | @Component 6 | struct linysTimeoutButtonWithText { 7 | // Info 8 | @State desc_text: ResourceStr = 'desc'; 9 | @State button_text: ResourceStr = ' OwO '; 10 | // Settings / Accessibility 11 | @StorageLink('preferred_hand_left_or_right') preferred_hand: string = 'right'; 12 | onExecution?: () => void; 13 | 14 | build() { 15 | Row({ space: 10 }) { 16 | Row() { 17 | linysText({ text: this.desc_text, max_lines: 10 }) 18 | } // Text 19 | .justifyContent(this.preferred_hand == 'right' ? FlexAlign.End : FlexAlign.Start) 20 | .animation(animation_default()) 21 | .layoutWeight(1) 22 | 23 | linysTimeoutButton({ 24 | text: this.button_text, 25 | onExecution: () => { 26 | if (this.onExecution) { 27 | this.onExecution(); 28 | } 29 | } 30 | }) // Button 31 | } 32 | .justifyContent(this.preferred_hand == 'right' ? FlexAlign.End : FlexAlign.Start) 33 | .direction(this.preferred_hand == 'right' ? Direction.Ltr : Direction.Rtl) 34 | .animation(animation_default()) 35 | .width('100%') 36 | } 37 | } 38 | 39 | export default linysTimeoutButtonWithText; -------------------------------------------------------------------------------- /home/src/main/ets/components/texts/linysSymbol.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, click_effect_default, fontSize_Icon_Button } from '../../hosts/bunch_of_defaults'; 2 | 3 | // @Reusable // Will this affect anything? // Weird layout bugs 4 | @Component 5 | struct linysSymbol { 6 | @Prop symbol_glyph_target: string = 'sys.symbol.arrow_left'; 7 | // Colors 8 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 9 | // Status 10 | @Prop color: ResourceColor = ""; 11 | @Prop font_weight: FontWeight = FontWeight.Regular; 12 | @Prop font_size: number | undefined = undefined; 13 | // Settings 14 | can_hover: boolean = false; 15 | radius: number = 10; 16 | // UI 17 | @State isHov: boolean = false; 18 | 19 | build() { 20 | SymbolGlyph($r(this.symbol_glyph_target)) 21 | .fontWeight(this.font_weight) 22 | .fontSize(this.font_size || fontSize_Icon_Button()) 23 | .fontColor([this.color == "" ? this.color_current_font : this.color]) 24 | .brightness(this.isHov ? 1.1 : 1) 25 | .animation(animation_default()) 26 | .padding(this.can_hover ? 5 : 0) 27 | .borderRadius(this.can_hover ? this.radius : 0) 28 | .clickEffect(click_effect_default()) 29 | .onHover((isHv) => { 30 | if (this.can_hover) { 31 | this.isHov = isHv; 32 | } 33 | }) 34 | } 35 | } 36 | 37 | export default linysSymbol -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysCapsuleButtonWithText.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import linysText from '../texts/linysText'; 3 | import linysCapsuleButton from './linysCapsuleButton'; 4 | 5 | // @Reusable 6 | @Component 7 | struct linysCapsuleButtonWithText { 8 | // Info 9 | @Prop desc_text: ResourceStr = 'desc'; 10 | @Prop button_text: ResourceStr = ' OwO '; 11 | // Settings / Accessibility 12 | @StorageLink('preferred_hand_left_or_right') preferred_hand: string = 'right'; 13 | onExecution?: () => void; 14 | 15 | build() { 16 | Row({ space: 10 }) { 17 | Row() { 18 | linysText({ text: this.desc_text, max_lines: 10 }) 19 | .animation(animation_default()) 20 | } // Text 21 | // .opacity(0.9) 22 | .justifyContent(this.preferred_hand == 'right' ? FlexAlign.End : FlexAlign.Start) 23 | .animation(animation_default()) 24 | .layoutWeight(1) 25 | 26 | linysCapsuleButton({ text: this.button_text })// Button 27 | .onClick(() => { 28 | if (this.onExecution) { 29 | this.onExecution(); 30 | } 31 | }) 32 | } 33 | .justifyContent(this.preferred_hand == 'right' ? FlexAlign.End : FlexAlign.Start) 34 | .direction(this.preferred_hand == 'right' ? Direction.Ltr : Direction.Rtl) 35 | .animation(animation_default()) 36 | .width('100%') 37 | } 38 | } 39 | 40 | export default linysCapsuleButtonWithText; -------------------------------------------------------------------------------- /home/src/main/ets/components/texts/linysTextTitle.ets: -------------------------------------------------------------------------------- 1 | import lazy { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import { fontSize_Large } from '../../hosts/bunch_of_defaults'; 3 | 4 | // @Reusable 5 | @Component 6 | struct linysTextTitle { 7 | // Colors 8 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 9 | @Prop text: ResourceStr = $r('app.string.HomeAbility_label'); 10 | @Prop color: ResourceColor = ""; 11 | @Prop max_lines: number = 1; 12 | @Prop font_size: number | undefined = undefined; 13 | // UI 14 | can_hover: boolean = false; 15 | @State isHov: boolean = false; 16 | 17 | build() { 18 | Text(this.text) 19 | .brightness(this.isHov ? 1.1 : 1) 20 | .fontSize(this.font_size || fontSize_Large()) 21 | .animation(this.can_hover ? animation_default() : { duration: 0 }) 22 | .padding(this.can_hover ? { 23 | left: 7, 24 | right: 7, 25 | top: 5, 26 | bottom: 5 27 | } : 0) 28 | .borderRadius(this.can_hover ? 7.5 : 0) 29 | .fontColor(this.color == "" ? this.color_current_font : this.color) 30 | .fontWeight(FontWeight.Bold) 31 | .maxLines(this.max_lines) 32 | .textOverflow({ overflow: TextOverflow.Ellipsis }) 33 | .onHover((isHv) => { 34 | if (this.can_hover) { 35 | this.isHov = isHv; 36 | } 37 | }) 38 | } 39 | } 40 | 41 | export default linysTextTitle; 42 | -------------------------------------------------------------------------------- /home/src/main/ets/utils/link_tools.ets: -------------------------------------------------------------------------------- 1 | import { common, OpenLinkOptions } from '@kit.AbilityKit'; 2 | import { uniformTypeDescriptor as utd } from '@kit.ArkData'; 3 | import { systemShare } from '@kit.ShareKit'; 4 | import { BusinessError } from '@kit.BasicServicesKit'; 5 | 6 | /** 7 | * Opens uri with filemanager. 8 | * @param uri the uri. 9 | * */ 10 | export function open_file_uri(uri: string, context: Context) { 11 | // Open folder 12 | let link: string = 'filemanager://openDirectory'; 13 | let openLinkOptions: OpenLinkOptions = { 14 | parameters: { 15 | 'fileUri': uri 16 | } 17 | }; 18 | try { 19 | (context as common.UIAbilityContext).openLink(link, openLinkOptions) 20 | } catch (e) { 21 | console.error('[link_tools] ' + e); 22 | } 23 | } 24 | 25 | /** 26 | * Shares a link through system share. 27 | * @param link The link. 28 | * @param title The title. 29 | * */ 30 | export function share_link(link: string, title: string, context: Context) { 31 | let data: systemShare.SharedData = new systemShare.SharedData({ 32 | utd: utd.UniformDataType.HYPERLINK, 33 | description: link, 34 | title: title, 35 | content: link 36 | }); 37 | let controller: systemShare.ShareController = new systemShare.ShareController(data); 38 | controller.show((context as common.UIAbilityContext), { 39 | previewMode: systemShare.SharePreviewMode.DEFAULT, 40 | selectionMode: systemShare.SelectionMode.BATCH 41 | }).catch((e: BusinessError) => { 42 | console.error('[share_link] Error: ' + JSON.stringify(e)); 43 | }); 44 | } -------------------------------------------------------------------------------- /home/src/main/resources/resfile/what/Paw.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /home/src/main/ets/utils/preview_tools.ets: -------------------------------------------------------------------------------- 1 | import { BusinessError } from '@kit.BasicServicesKit'; 2 | import { filePreview } from '@kit.PreviewKit'; 3 | import { common } from '@kit.AbilityKit'; 4 | import { meowContext } from './environment_tools'; 5 | 6 | const image_suffixes = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg']; 7 | const image_mimes = ['jpeg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'svg+xml']; 8 | 9 | export function preview_image(uri: string, storage: LocalStorage) { 10 | // Determine file name 11 | const split_name_result = uri.split('/'); 12 | const file_name = split_name_result[split_name_result.length-1]; 13 | // Determine file suffix 14 | const file_suffix_result = file_name.split('.'); 15 | const file_suffix = file_suffix_result[file_suffix_result.length-1]; 16 | // Determine mime 17 | let mime: string = 'image/' + image_mimes[image_suffixes.indexOf(file_suffix.toLowerCase())]; 18 | // Show 19 | let displayInfo: filePreview.DisplayInfo = { 20 | x: 100, 21 | y: 100, 22 | width: 800, 23 | height: 800 24 | }; 25 | let fileInfo: filePreview.PreviewInfo = { 26 | title: '(ฅ^・ﻌ・^)ฅ - ' + file_name, 27 | uri: uri, 28 | mimeType: mime 29 | }; 30 | 31 | let context = storage.get('context') as Context; 32 | filePreview.openPreview(context, fileInfo, displayInfo).then(() => { 33 | console.info('[preview_image] Succeeded in opening preview'); 34 | }).catch((err: BusinessError) => { 35 | console.error(`[preview_image] Failed to open preview, err.code = ${err.code}, err.message = ${err.message}`); 36 | }); 37 | } 38 | 39 | -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysCapsuleButton.ets: -------------------------------------------------------------------------------- 1 | import { 2 | animation_default, capsule_bar_height, click_effect_default, fontSize_Large 3 | } from '../../hosts/bunch_of_defaults'; 4 | 5 | // @Reusable 6 | // Can I? 7 | @Component 8 | struct linysCapsuleButton { 9 | @Prop text: ResourceStr = ""; 10 | // Colors 11 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 12 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 13 | @StorageLink('color_current_back') color_current_back: ResourceColor = $r('app.color.font_color_title'); 14 | @Prop color_button: ResourceColor = ""; 15 | @Prop color_text: ResourceColor = ""; 16 | @StorageLink('effects') effects: boolean = false; 17 | opa: number = 0.9; 18 | font_weight: FontWeight = FontWeight.Medium; 19 | 20 | build() { 21 | Button(this.text) 22 | .opacity(this.effects ? 1 : this.opa) 23 | .type(ButtonType.Capsule) 24 | .backgroundColor(this.color_button == "" ? this.color_current_back : this.color_button) 25 | .fontColor(this.color_text == "" ? (this.effects ? this.color_current_font : this.color_current_primary) : this.color_text) 26 | .fontSize(fontSize_Large()) 27 | .fontWeight(this.font_weight) 28 | .height(capsule_bar_height()) 29 | .border({ 30 | width: 2, 31 | color: "transparent" 32 | }) 33 | .clickEffect(click_effect_default()) 34 | .animation(animation_default()) 35 | } 36 | } 37 | 38 | export default linysCapsuleButton; -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysWindowButton.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../../hosts/bunch_of_defaults'; 2 | 3 | @Component 4 | struct linysWindowButton { 5 | @State hover_on: boolean = false; 6 | @State press_on: boolean = false; 7 | @Prop symbol: Resource = $r('sys.symbol.plus'); 8 | // Colors 9 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 10 | // Environment 11 | @LocalStorageLink('my_window_alias') my_window_alias: string = ''; 12 | @LocalStorageLink('on_focus') on_focus: boolean = true; 13 | // Actions 14 | execution?: () => void; 15 | 16 | build() { 17 | Row() { 18 | SymbolGlyph(this.symbol) 19 | .fontWeight(FontWeight.Normal) 20 | .fontSize(this.hover_on ? 18 : 16) 21 | .fontColor([this.color_current_font]) 22 | } 23 | .alignItems(VerticalAlign.Center) 24 | .justifyContent(FlexAlign.Center) 25 | .animation(animation_default()) 26 | .backgroundColor(this.hover_on ? $r('sys.color.comp_background_secondary') : undefined) 27 | .brightness(this.press_on ? 1.08 : undefined) 28 | .width(28.5) 29 | .height(28.5) 30 | .borderRadius(4) 31 | .onClick(() => { 32 | if (this.execution) { 33 | this.execution(); 34 | } 35 | }) 36 | .onTouch((e) => { 37 | if (e.type == TouchType.Down) { 38 | this.press_on = true; 39 | } else { 40 | this.press_on = false; 41 | } 42 | }) 43 | .opacity(this.on_focus ? 1 : 0.4) 44 | .onHover((h) => { 45 | this.hover_on = h; 46 | }) 47 | } 48 | } 49 | 50 | export default linysWindowButton; -------------------------------------------------------------------------------- /home/src/main/ets/homeabilitystage/HomeAbilityStage.ets: -------------------------------------------------------------------------------- 1 | import { AbilityStage, Want } from '@kit.AbilityKit'; 2 | 3 | export default class HomeAbilityStage extends AbilityStage { 4 | onCreate(): void { 5 | AppStorage.setOrCreate('context', this.context); 6 | } 7 | 8 | onAcceptWant(want: Want): string { 9 | let instanceKey = ''; 10 | let windowIds: string[] | undefined = AppStorage.get('windowIds'); 11 | 12 | if (want.parameters) { 13 | // instanceKey 14 | let key = want.parameters.instanceKey; 15 | if (key) { 16 | // Specially call for a new window. 17 | console.log('[Meow][HomeAbility][onAcceptWant]! Key = [' + key + ']'); 18 | instanceKey = 'Meow_' + key; 19 | } else { 20 | console.log('[Meow][HomeAbility][onAcceptWant]! Key is undefined.'); 21 | if (windowIds) { 22 | // Call the last of the already opened windows. 23 | let focus_window_id = AppStorage.get('THE_WANT_HANDLER_OF_THE_WINDOWS') as string; 24 | if (focus_window_id == '') { 25 | // That window is closed; 26 | instanceKey = windowIds[windowIds.length-1]; 27 | } else { 28 | // Give want to that window. 29 | instanceKey = focus_window_id; 30 | } 31 | } else { 32 | // Initial key for the very first window of the entire app. 33 | instanceKey = 'Meow_meow'; 34 | } 35 | } 36 | } 37 | // So that HomeAbility class could read and use this as window id. 38 | AppStorage.setOrCreate('LAST_WINDOW_INSTANCE_KEY', instanceKey); 39 | 40 | // Report back 41 | return instanceKey; 42 | } 43 | } -------------------------------------------------------------------------------- /home/src/main/ets/blocks/contents/meowWhatsNew.ets: -------------------------------------------------------------------------------- 1 | import linysText from '../../components/texts/linysText'; 2 | import { animation_default } from '../../hosts/bunch_of_defaults'; 3 | import { resource_to_string } from '../../utils/resource_tools'; 4 | 5 | @Component 6 | struct meowWhatsNew { 7 | @State new_things_resource: Resource = $r("app.string.Whats_new_content"); 8 | @State new_things: string = resource_to_string(this.new_things_resource); 9 | @State new_things_list: string[] = this.new_things.split('\n'); 10 | 11 | build() { 12 | Column({ space: 8 }) { 13 | ForEach(this.new_things_list, (text: string, index: number) => { 14 | Row({ space: 5 }) { 15 | linysText({ text: (index + 1).toString() + '.' }) 16 | linysText({ 17 | text: text, 18 | max_lines: 48 19 | }) 20 | .opacity(0.8) 21 | .layoutWeight(1) 22 | } 23 | // .width('90%') 24 | .alignItems(VerticalAlign.Top) 25 | }) 26 | } 27 | .alignItems(HorizontalAlign.Start) 28 | .padding(10) 29 | .borderRadius(13.5) 30 | .backgroundColor($r('sys.color.comp_background_tertiary')) 31 | .width("100%") 32 | .animation(animation_default()) 33 | .onAreaChange(() => { 34 | this.update_resource_string(); 35 | }) 36 | } 37 | 38 | /** 39 | * Convert the Resource to string again. 40 | * 41 | * Usually called to deal with the change of language. 42 | * */ 43 | update_resource_string() { 44 | this.new_things = resource_to_string(this.new_things_resource); 45 | this.new_things_list = this.new_things.split('\n'); 46 | } 47 | } 48 | 49 | export default meowWhatsNew; -------------------------------------------------------------------------------- /home/src/test/LocalUnit.test.ets: -------------------------------------------------------------------------------- 1 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; 2 | 3 | export default function localUnitTest() { 4 | describe('localUnitTest', () => { 5 | // Defines a test suite. Two parameters are supported: test suite name and test suite function. 6 | beforeAll(() => { 7 | // Presets an action, which is performed only once before all test cases of the test suite start. 8 | // This API supports only one parameter: preset action function. 9 | }); 10 | beforeEach(() => { 11 | // Presets an action, which is performed before each unit test case starts. 12 | // The number of execution times is the same as the number of test cases defined by **it**. 13 | // This API supports only one parameter: preset action function. 14 | }); 15 | afterEach(() => { 16 | // Presets a clear action, which is performed after each unit test case ends. 17 | // The number of execution times is the same as the number of test cases defined by **it**. 18 | // This API supports only one parameter: clear action function. 19 | }); 20 | afterAll(() => { 21 | // Presets a clear action, which is performed after all test cases of the test suite end. 22 | // This API supports only one parameter: clear action function. 23 | }); 24 | it('assertContain', 0, () => { 25 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. 26 | let a = 'abc'; 27 | let b = 'b'; 28 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions. 29 | expect(a).assertContain(b); 30 | expect(a).assertEqual(a); 31 | }); 32 | }); 33 | } -------------------------------------------------------------------------------- /home/src/main/ets/blocks/contents/meowCreditsRepos.ets: -------------------------------------------------------------------------------- 1 | import linysText from '../../components/texts/linysText'; 2 | import { animation_default } from '../../hosts/bunch_of_defaults'; 3 | import { resource_to_string } from '../../utils/resource_tools'; 4 | import { common } from '@kit.AbilityKit'; 5 | 6 | @Component 7 | struct meowCreditsRepos { 8 | @State credits_repos_and_projs_resource: Resource = $r("app.string.Credits_repos_and_projs"); 9 | @State credits_repos_and_projs: string = resource_to_string(this.credits_repos_and_projs_resource); 10 | @State credits_repos_and_projs_list: string[] = this.credits_repos_and_projs.split('\n'); 11 | 12 | build() { 13 | Column({ space: 8 }) { 14 | linysText({ 15 | text: $r('app.string.Credits'), 16 | max_lines: 3 17 | }) 18 | 19 | ForEach(this.credits_repos_and_projs_list, (text: string, index: number) => { 20 | linysText({ 21 | text: text, 22 | max_lines: 48 23 | }) 24 | .opacity(0.8) 25 | }) 26 | } 27 | .alignItems(HorizontalAlign.Start) 28 | .padding(10) 29 | .borderRadius(13.5) 30 | .backgroundColor($r('sys.color.comp_background_tertiary')) 31 | .width("100%") 32 | .animation(animation_default()) 33 | .onAreaChange(() => { 34 | this.update_resource_string(); 35 | }) 36 | } 37 | 38 | /** 39 | * Convert the Resource to string again. 40 | * 41 | * Usually called to deal with the change of language. 42 | * */ 43 | update_resource_string() { 44 | this.credits_repos_and_projs = resource_to_string(this.credits_repos_and_projs_resource); 45 | this.credits_repos_and_projs_list = this.credits_repos_and_projs.split('\n'); 46 | } 47 | } 48 | 49 | export default meowCreditsRepos; 50 | -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/quicks/woofQuickUA.ets: -------------------------------------------------------------------------------- 1 | import meowUAManager from '../../blocks/panels/meowUAManager'; 2 | import { animation_default } from '../../hosts/bunch_of_defaults'; 3 | import { bunch_of_user_agents } from '../../hosts/bunch_of_user_agents'; 4 | import lazy woofControlFrame from '../woofControlFrame'; 5 | 6 | @CustomDialog 7 | struct woofQuickUA { 8 | controller: CustomDialogController; 9 | // Color 10 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 11 | // Count 12 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 13 | 14 | aboutToAppear(): void { 15 | this.opened_dialog_controllers.push(this.controller); 16 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 17 | } 18 | 19 | aboutToDisappear(): void { 20 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 21 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 22 | } 23 | 24 | build() { 25 | woofControlFrame({ 26 | title: $r('app.string.Settings_general_custom_ua'), 27 | controller: this.controller 28 | }) { 29 | Scroll() { 30 | meowUAManager() 31 | } 32 | .borderRadius(13.5) 33 | .layoutWeight(1) 34 | .scrollable(ScrollDirection.Vertical) 35 | .edgeEffect(EdgeEffect.Spring) 36 | // .backgroundColor(this.effects ? undefined : this.color_current_primary) 37 | .constraintSize({ maxHeight: 92.5 + 48.5 * bunch_of_user_agents.list_of_user_agents.length }) 38 | .animation(animation_default()) 39 | } 40 | } 41 | } 42 | 43 | export default woofQuickUA; -------------------------------------------------------------------------------- /home/src/main/ets/utils/any_concurrent_tools.ets: -------------------------------------------------------------------------------- 1 | import { get_folder_size_Sync } from './storage_tools'; 2 | import { taskpool } from '@kit.ArkTS'; 3 | import { getGraphicsMemory_sync, getPrivateDirty_sync, getPss_sync, getSharedDirty_sync, getVss_sync } from './performance_tools'; 4 | import { meowContext } from './environment_tools'; 5 | 6 | @Concurrent 7 | function get_sandbox_folder_size_concurrent(path: string): number { 8 | return get_folder_size_Sync(path, true); 9 | } 10 | 11 | /** 12 | * Get size of any folder, including its contents, in sandbox in a concurrent way. 13 | * @param folder_path the folder path. 14 | * @param sandbox_root to set sandbox root as root or not. 15 | * @returns a number of folder size. 16 | * */ 17 | export async function get_sandbox_folder_size(folder_path: string, sandbox_root: boolean): Promise { 18 | let path: string; 19 | if (sandbox_root) { 20 | const filesDir = meowContext().filesDir; 21 | path = filesDir + '/' + folder_path; 22 | } else { 23 | path = folder_path; 24 | } 25 | return await taskpool.execute(get_sandbox_folder_size_concurrent, path) as number; 26 | } 27 | 28 | export async function getPss(): Promise { 29 | return await taskpool.execute(getPss_sync) as bigint; 30 | } 31 | 32 | export async function getVss(): Promise { 33 | return await taskpool.execute(getVss_sync) as bigint; 34 | } 35 | 36 | export async function getSharedDirty(): Promise { 37 | return await taskpool.execute(getSharedDirty_sync) as bigint; 38 | } 39 | 40 | export async function getPrivateDirty(): Promise { 41 | return await taskpool.execute(getPrivateDirty_sync) as bigint; 42 | } 43 | 44 | export async function getVRAM(): Promise { 45 | return await taskpool.execute(getGraphicsMemory_sync) as number; 46 | } -------------------------------------------------------------------------------- /home/src/main/ets/components/toggles/linysSwitchWithText.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import linysText from '../texts/linysText'; 3 | 4 | // @Reusable 5 | @Component 6 | struct linysSwitchWithText { 7 | // Links 8 | @Prop text: ResourceStr = ""; 9 | @Link toggle_state: boolean; 10 | onExecution?: () => void; 11 | // Settings / Accessibility 12 | @StorageLink('preferred_hand_left_or_right') preferred_hand: string = 'right'; 13 | // Colors 14 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 15 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 16 | @StorageLink('color_current_back') color_current_back: ResourceColor = $r('app.color.font_color_title'); 17 | @StorageLink('effects') effects: boolean = false; 18 | 19 | build() { 20 | Row({ space: 10 }) { 21 | Row() { 22 | linysText({ text: this.text, max_lines: 5 }) 23 | } 24 | // .opacity(0.9) 25 | .layoutWeight(1) 26 | .justifyContent(FlexAlign.Start) 27 | 28 | Toggle({ isOn: this.toggle_state, type: ToggleType.Switch }) 29 | .switchPointColor(this.effects ? this.color_current_font : this.color_current_secondary) 30 | .opacity(!this.toggle_state && this.effects ? 0.85 : 1) 31 | .selectedColor(this.color_current_back) 32 | .onChange(isOn => { 33 | this.toggle_state = isOn; 34 | if (this.onExecution) { 35 | this.onExecution(); 36 | } 37 | }) 38 | } // Toggle 39 | .direction(this.preferred_hand == 'right' ? Direction.Ltr : Direction.Rtl) 40 | .animation(animation_default()) 41 | .width("100%") 42 | } 43 | } 44 | 45 | export default linysSwitchWithText; -------------------------------------------------------------------------------- /home/src/main/ets/components/linysFileNameErrorDisplay.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from "../hosts/bunch_of_defaults"; 2 | import linysText from "./texts/linysText"; 3 | 4 | @Component 5 | struct linysFileNameErrorDisplay { 6 | @Prop file_name: string = 'uwu'; 7 | @Prop error_code: number = 0; 8 | chars: string[] = ['', '', '\\', '/', ':', '*', '?', '\'', '<', '>', '|']; 9 | 10 | build() { 11 | Column({ space: 10 }) { 12 | linysText({ 13 | text: $r('app.string.Index_downloads_illegal_chars_in_filename'), 14 | max_lines: 3, 15 | is_description: true, 16 | is_expanded: true 17 | }) // File name error prompt 18 | .visibility(this.error_code > 0 ? Visibility.Visible : Visibility.None) 19 | .animation(animation_default()) 20 | 21 | Row() { 22 | linysText({ 23 | text: $r('app.string.Index_downloads_filename_length'), 24 | max_lines: 3, 25 | }) 26 | linysText({ 27 | text: this.file_name.length.toString(), 28 | max_lines: 3, 29 | }) 30 | } // File name too long 31 | .visibility(this.error_code == 1 ? Visibility.Visible : Visibility.None) 32 | .animation(animation_default()) 33 | 34 | Row() { 35 | linysText({ 36 | text: $r('app.string.Index_downloads_filename_illegal'), 37 | max_lines: 3, 38 | }) 39 | linysText({ 40 | text: this.chars[this.error_code], 41 | max_lines: 3, 42 | }) 43 | } // File name illegal 44 | .visibility(this.error_code >= 2 ? Visibility.Visible : Visibility.None) 45 | .animation(animation_default()) 46 | } 47 | .visibility(this.error_code > 0 ? Visibility.Visible : Visibility.None) 48 | .alignItems(HorizontalAlign.Start) 49 | } 50 | } 51 | 52 | export default linysFileNameErrorDisplay -------------------------------------------------------------------------------- /home/src/ohosTest/ets/test/Ability.test.ets: -------------------------------------------------------------------------------- 1 | import { hilog } from '@kit.PerformanceAnalysisKit'; 2 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; 3 | 4 | export default function abilityTest() { 5 | describe('ActsAbilityTest', () => { 6 | // Defines a test suite. Two parameters are supported: test suite name and test suite function. 7 | beforeAll(() => { 8 | // Presets an action, which is performed only once before all test cases of the test suite start. 9 | // This API supports only one parameter: preset action function. 10 | }) 11 | beforeEach(() => { 12 | // Presets an action, which is performed before each unit test case starts. 13 | // The number of execution times is the same as the number of test cases defined by **it**. 14 | // This API supports only one parameter: preset action function. 15 | }) 16 | afterEach(() => { 17 | // Presets a clear action, which is performed after each unit test case ends. 18 | // The number of execution times is the same as the number of test cases defined by **it**. 19 | // This API supports only one parameter: clear action function. 20 | }) 21 | afterAll(() => { 22 | // Presets a clear action, which is performed after all test cases of the test suite end. 23 | // This API supports only one parameter: clear action function. 24 | }) 25 | it('assertContain', 0, () => { 26 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. 27 | hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); 28 | let a = 'abc'; 29 | let b = 'b'; 30 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions. 31 | expect(a).assertContain(b); 32 | expect(a).assertEqual(a); 33 | }) 34 | }) 35 | } -------------------------------------------------------------------------------- /home/src/main/ets/components/texts/linysLink.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, fontSize_Normal } from '../../hosts/bunch_of_defaults'; 2 | import { LengthMetrics } from '@kit.ArkUI'; 3 | 4 | @Component 5 | struct linysLink { 6 | // Colors 7 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 8 | @Prop text: ResourceStr = $r('app.string.HomeAbility_label'); 9 | @Prop color: ResourceColor = ""; 10 | @Prop max_lines: number = 1; 11 | @Prop font_weight: FontWeight = FontWeight.Regular; 12 | @State link: string = "huawei.com"; 13 | @LocalStorageLink('universal_new_tab_gateway') universal_new_tab_gateway: (string | boolean)[] = ['', false]; 14 | // In fact this param is designed for closing some opened panel 15 | @Link will_be_reversed_boolean: boolean; 16 | @State isHov: boolean = false; 17 | 18 | build() { 19 | Text(this.text) 20 | .brightness(this.isHov ? 1.1 : 1) 21 | .animation(animation_default()) 22 | .padding({ 23 | left: 7, 24 | right: 7, 25 | top: 5, 26 | bottom: 5 27 | }) 28 | .borderRadius(7.5) 29 | .fontColor(this.color == "" ? this.color_current_font : this.color) 30 | .fontSize(fontSize_Normal()) 31 | .fontWeight(this.font_weight) 32 | .textAlign(TextAlign.Start) 33 | .maxLines(this.max_lines) 34 | .lineSpacing(LengthMetrics.vp(4)) 35 | .textOverflow({ overflow: TextOverflow.Ellipsis }) 36 | .onClick(() => { 37 | this.universal_new_tab_gateway = [this.link, false]; 38 | this.will_be_reversed_boolean = !this.will_be_reversed_boolean; 39 | }) 40 | .onHover((isHv) => { 41 | this.isHov = isHv; 42 | }) 43 | .accessibilityText(this.text + ',' + this.link) 44 | .accessibilityDescription($r('app.string.Accessibility_desc_linysLink')) 45 | } 46 | } 47 | 48 | export default linysLink -------------------------------------------------------------------------------- /home/src/main/ets/workers/Homepage_background_loader.ets: -------------------------------------------------------------------------------- 1 | import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; 2 | import { arrayBuffer_2_sendable_pixelMap, sandbox_read_arrayBuffer } from '../utils/storage_tools'; 3 | 4 | const workerPort: ThreadWorkerGlobalScope = worker.workerPort; 5 | let pathDir: string = ''; 6 | 7 | /** 8 | * Defines the event handler to be called when the worker thread receives a message sent by the host thread. 9 | * The event handler is executed in the worker thread. 10 | * 11 | * @param event message data 12 | */ 13 | workerPort.onmessage = async (event: MessageEvents) => { 14 | if (typeof event.data == 'string') { 15 | // Is pathDir 16 | pathDir = event.data as string; 17 | } 18 | // Start 19 | console.log("[Meow][meowAppSettings] Start loading homepage background!"); 20 | sandbox_read_arrayBuffer('homepage_background_arrayBuffer', pathDir)?.then((homepage_background_array_buffer) => { 21 | console.log("[Meow][meowAppSettings] Finished reading homepage background!"); 22 | if (homepage_background_array_buffer) { 23 | arrayBuffer_2_sendable_pixelMap(homepage_background_array_buffer).then((pm) => { 24 | console.log("[Meow][meowAppSettings] Finished processing homepage background!"); 25 | workerPort.postMessageWithSharedSendable(pm); 26 | }) 27 | } 28 | }); 29 | }; 30 | 31 | /** 32 | * Defines the event handler to be called when the worker receives a message that cannot be deserialized. 33 | * The event handler is executed in the worker thread. 34 | * 35 | * @param event message data 36 | */ 37 | workerPort.onmessageerror = (event: MessageEvents) => { 38 | }; 39 | 40 | /** 41 | * Defines the event handler to be called when an exception occurs during worker execution. 42 | * The event handler is executed in the worker thread. 43 | * 44 | * @param event error message 45 | */ 46 | workerPort.onerror = (event: ErrorEvent) => { 47 | }; -------------------------------------------------------------------------------- /home/src/main/ets/components/linysLockSlider.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../hosts/bunch_of_defaults'; 2 | import linysLockToggle from './toggles/linysLockToggle'; 3 | import linysText from './texts/linysText'; 4 | 5 | // @Reusable 6 | @Component 7 | struct linysLockSlider { 8 | @State is_locked: boolean = true; 9 | @Prop slider_min: number = 0; 10 | @Prop slider_max: number = 100; 11 | @Prop display: string | undefined = undefined; 12 | @Link slider_value: number; 13 | // Colors 14 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 15 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 16 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 17 | // Operations 18 | onTouchUp?: () => void; 19 | 20 | build() { 21 | Row({ space: 5 }) { 22 | linysLockToggle({ locked: this.is_locked }) 23 | 24 | Slider({ 25 | min: this.slider_min, 26 | max: this.slider_max, 27 | value: this.slider_value, 28 | style: SliderStyle.InSet, 29 | }) 30 | .enabled(!this.is_locked) 31 | .opacity(!this.is_locked ? 1 : 0.5) 32 | .animation(animation_default()) 33 | .layoutWeight(1) 34 | .blockColor(this.color_current_primary) 35 | .selectedColor(this.color_current_font) 36 | .onChange((value) => { 37 | this.slider_value = value; 38 | }) 39 | .onTouch(e => { 40 | if (e.type == TouchType.Up) { 41 | // this.bunch_of_settings.set('animation_damping_coefficient', 42 | // this.slider_value); 43 | if (this.onTouchUp) { 44 | this.onTouchUp(); 45 | } 46 | } 47 | }) 48 | linysText({ text: this.display ? this.display : this.slider_value.toString() }) 49 | .margin({ right: 5 }) 50 | } 51 | } 52 | } 53 | 54 | export default linysLockSlider; -------------------------------------------------------------------------------- /home/src/main/ets/utils/permission_tools.ets: -------------------------------------------------------------------------------- 1 | import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; 2 | import { meowContext } from './environment_tools'; 3 | 4 | // import { webview } from '@kit.ArkWeb'; 5 | 6 | // webview.WebviewController.setWebDebuggingAccess(true); 7 | let atManager = abilityAccessCtrl.createAtManager(); 8 | 9 | /** 10 | * Requests permission from user. 11 | * @param p Permissions[] 12 | * */ 13 | export function request_user_permission(context:common.UIAbilityContext ,p: Permissions[]) { 14 | if (p.length == 0) { 15 | return; 16 | } 17 | atManager.requestPermissionsFromUser(context, p); 18 | } 19 | 20 | /** 21 | * Analyzes a list of protected resource types and return their according permissions. 22 | * @param prt the list of protected resource types. 23 | * @returns a list of Permissions. 24 | * */ 25 | export function permissions_for_ProtectedResourceTypes(prt: string[]) { 26 | let permissions: Permissions[] = []; 27 | for (let index = 0; index < prt.length; index++) { 28 | const ProtectedResourceType = prt[index]; 29 | if (ProtectedResourceType == 'TYPE_AUDIO_CAPTURE') { 30 | permissions.push('ohos.permission.MICROPHONE'); 31 | } 32 | if (ProtectedResourceType == 'TYPE_VIDEO_CAPTURE') { 33 | permissions.push('ohos.permission.CAMERA'); 34 | } 35 | } 36 | return permissions; 37 | } 38 | 39 | /** 40 | * Analyzes a list of protected resource types and return their according descriptions in human language. 41 | * @param prt the list of protected resource types. 42 | * @returns list of ResourceStr. 43 | * */ 44 | export function desc_for_ProtectedResourceTypes(prt: string[]) { 45 | let result: ResourceStr[] = []; 46 | for (let index = 0; index < prt.length; index++) { 47 | const r = prt[index]; 48 | if ($r('app.string.protected_resource_'.concat(r))) { 49 | result.push($r('app.string.protected_resource_'.concat(r))); 50 | } else { 51 | result.push('[Error] Unknown Protected Resource Type? ' + r); 52 | } 53 | } 54 | return result; 55 | } -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/woofControlFrame.ets: -------------------------------------------------------------------------------- 1 | import linysSymbol from '../components/texts/linysSymbol'; 2 | import linysTextTitle from '../components/texts/linysTextTitle'; 3 | import { animation_default } from '../hosts/bunch_of_defaults'; 4 | 5 | @Component 6 | struct woofControlFrame { 7 | // Environment 8 | @LocalStorageLink('tablet_mode') tablet_mode: boolean = false; 9 | @LocalStorageLink('compact_mode') compact_mode: boolean = false; 10 | // Pass in content components 11 | @BuilderParam content_section: () => void; 12 | // Information 13 | title: ResourceStr = 'Meow'; 14 | @Prop space_column: number = 2; 15 | @Prop show_title: boolean = true; 16 | // Control 17 | controller?: CustomDialogController; 18 | // Color 19 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 20 | // animation 21 | @State off_y_contents: number = 120; 22 | @StorageLink('effects') effects: boolean = false; 23 | 24 | build() { 25 | Column({ space: this.space_column }) { 26 | Row() { 27 | linysTextTitle({ 28 | text: this.title, 29 | max_lines: 3 30 | }) 31 | .layoutWeight(1) 32 | linysSymbol({ 33 | symbol_glyph_target: 'sys.symbol.xmark', 34 | can_hover: true, 35 | }) 36 | .onClick(() => { 37 | if (this.controller) { 38 | this.controller.close(); 39 | } 40 | }) 41 | } 42 | .visibility(this.show_title ? Visibility.Visible : Visibility.None) 43 | .animation(animation_default()) 44 | 45 | // Contents 46 | this.content_section(); 47 | } 48 | .offset({ y: this.off_y_contents }) 49 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 50 | .padding(this.compact_mode ? 10 : 15) 51 | .animation(animation_default()) 52 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 53 | .onAppear(() => { 54 | this.off_y_contents = 0; 55 | }) 56 | } 57 | } 58 | 59 | export default woofControlFrame; -------------------------------------------------------------------------------- /home/src/main/ets/components/linysProgress.ets: -------------------------------------------------------------------------------- 1 | @Component 2 | struct linysProgress { 3 | // Colors 4 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 5 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 6 | @State bar_height: Length = 8; 7 | @State bar_width: Length = "100%"; 8 | @Watch('on_update') @Prop is_loading: boolean = false; 9 | @Watch('on_update') @Prop percentage: number = 0; 10 | @State display_rate: number = 0.0; 11 | @State color_progress: ResourceColor = ""; 12 | @State color_undone: ResourceColor = ""; 13 | @State show: boolean = false; 14 | 15 | build() { 16 | Row() { 17 | Row() 18 | .height("100%") 19 | .layoutWeight(this.display_rate) 20 | .backgroundColor(this.color_progress == "" ? this.color_current_font : this.color_progress) 21 | .animation({ duration: this.show ? 500 : 0, curve: Curve.EaseInOut }) 22 | 23 | Row() 24 | .height("100%") 25 | .layoutWeight(1 - (this.display_rate)) 26 | .backgroundColor(this.color_undone == "" ? this.color_current_primary : this.color_undone) 27 | .animation({ duration: this.show ? 500 : 0, curve: Curve.EaseInOut }) 28 | } 29 | .height(this.show ? this.bar_height : 0) 30 | .width(this.bar_width) 31 | .animation({ duration: 200, curve: Curve.ExtremeDeceleration }) 32 | .accessibilityText(this.percentage?.toString() + "%") 33 | .accessibilityDescription($r('app.string.Accessibility_desc_linysProgress')) 34 | .accessibilityGroup(true) 35 | } 36 | 37 | on_update() { 38 | this.display_rate = this.percentage / 100 39 | if (this.is_loading) { 40 | this.show = true; 41 | } else { 42 | setTimeout(() => { 43 | // Auto hide after is not loading 44 | this.show = false; 45 | }, 500) 46 | setTimeout(() => { 47 | // Reset progress bar 48 | this.display_rate = 0; 49 | }, 800) 50 | } 51 | } 52 | } 53 | 54 | export default linysProgress -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/web/woofAlert.ets: -------------------------------------------------------------------------------- 1 | import linysConfirmDenyButtons from '../../components/buttons/linysConfirmDenyButtons'; 2 | import linysText from '../../components/texts/linysText'; 3 | import linysTextTitle from '../../components/texts/linysTextTitle'; 4 | 5 | @CustomDialog 6 | struct woofAlert { 7 | @Link event: OnAlertEvent | undefined; 8 | controller: CustomDialogController; 9 | // Colors 10 | @StorageLink('effects') effects: boolean = false; 11 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 12 | // Count 13 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 14 | 15 | aboutToAppear(): void { 16 | this.opened_dialog_controllers.push(this.controller); 17 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 18 | } 19 | 20 | aboutToDisappear(): void { 21 | this.event?.result.handleCancel(); 22 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 23 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 24 | } 25 | 26 | build() { 27 | Column({ space: 15 }) { 28 | linysTextTitle({ text: this.event?.url, max_lines: 2 }) // Page url 29 | .width("100%") 30 | 31 | linysText({ text: this.event?.message, max_lines: 999 }) // Prompt message 32 | .width("100%") 33 | 34 | linysConfirmDenyButtons({ 35 | onConfirm: () => { 36 | if (this.controller) { 37 | this.event?.result.handleConfirm(); 38 | this.controller.close(); 39 | } 40 | }, 41 | confirm_double_confirm: true 42 | }) // Buttons 43 | } 44 | .padding(20) 45 | .alignItems(HorizontalAlign.Start) 46 | .justifyContent(FlexAlign.Center) 47 | .width("100%") 48 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 49 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 50 | } 51 | } 52 | 53 | export default woofAlert; -------------------------------------------------------------------------------- /home/src/main/ets/blocks/contents/meowCreditsUsers.ets: -------------------------------------------------------------------------------- 1 | import linysText from '../../components/texts/linysText'; 2 | import { animation_default } from '../../hosts/bunch_of_defaults'; 3 | import { resource_to_string } from '../../utils/resource_tools'; 4 | 5 | @Component 6 | struct meowCreditsUsers { 7 | @State credits_devs_and_users_resource: Resource = $r("app.string.Credits_devs_and_users"); 8 | @State credits_devs_and_users: string = resource_to_string(this.credits_devs_and_users_resource); 9 | @State credits_devs_and_users_list: string[] = this.credits_devs_and_users.split('\n'); 10 | 11 | build() { 12 | Scroll() { 13 | Column({ space: 8 }) { 14 | linysText({ 15 | text: $r('app.string.Credits_issues'), 16 | max_lines: 10 17 | }) 18 | 19 | ForEach(this.credits_devs_and_users_list, (text: string, index: number) => { 20 | linysText({ 21 | text: text, 22 | max_lines: 48 23 | }) 24 | .opacity(0.8) 25 | }) 26 | } 27 | .alignItems(HorizontalAlign.Start) 28 | .padding(10) 29 | .borderRadius(13.5) 30 | .backgroundColor($r('sys.color.comp_background_tertiary')) 31 | .width("100%") 32 | .animation(animation_default()) 33 | .onAreaChange(() => { 34 | this.update_resource_string(); 35 | }) 36 | } 37 | // .nestedScroll({ scrollForward: NestedScrollMode.SELF_FIRST, scrollBackward: NestedScrollMode.SELF_FIRST }) 38 | .width("100%") 39 | .height(180) 40 | .scrollable(ScrollDirection.Vertical) 41 | .edgeEffect(EdgeEffect.Spring) 42 | .borderRadius(13.5) 43 | .animation(animation_default()) 44 | 45 | } 46 | 47 | /** 48 | * Convert the Resource to string again. 49 | * 50 | * Usually called to deal with the change of language. 51 | * */ 52 | update_resource_string() { 53 | this.credits_devs_and_users = resource_to_string(this.credits_devs_and_users_resource); 54 | this.credits_devs_and_users_list = this.credits_devs_and_users.split('\n'); 55 | } 56 | } 57 | 58 | export default meowCreditsUsers; 59 | -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysTimeoutButton.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, animation_popup_duration, capsule_bar_height, click_effect_default, fontSize_Large } from '../../hosts/bunch_of_defaults'; 2 | 3 | @Component 4 | struct linysTimeoutButton { 5 | @Prop text: ResourceStr = ' 󰃈 '; 6 | @StorageLink('effects') effects: boolean = false; 7 | // editing 8 | @State one_more_step: boolean = false; 9 | // Colors 10 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 11 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 12 | @StorageLink('color_current_back') color_current_back: ResourceColor = $r('app.color.font_color_title'); 13 | // Operations 14 | onExecution?: () => void; 15 | // Timeout 16 | confirm_timeout_id: number | undefined = undefined; 17 | opa: number = 0.9; 18 | 19 | build() { 20 | Button(this.text)// Reset 21 | .opacity(this.effects ? 1 : this.opa) 22 | .type(ButtonType.Capsule) 23 | // .useEffect(this.effects, EffectType.WINDOW_EFFECT) 24 | .backgroundColor(this.one_more_step ? this.color_current_primary : this.color_current_back) 25 | .fontColor(this.one_more_step ? this.color_current_font :(this.effects ? this.color_current_font : this.color_current_primary)) 26 | .border({ 27 | width: 2, 28 | color: this.one_more_step ? this.color_current_font : "transparent" 29 | }) 30 | .animation(animation_default()) 31 | .fontSize(fontSize_Large()) 32 | .fontWeight(FontWeight.Medium) 33 | .height(capsule_bar_height()) 34 | .clickEffect(click_effect_default()) 35 | .onClick(() => { 36 | if (this.one_more_step) { 37 | if (this.onExecution) { 38 | this.onExecution(); 39 | } 40 | this.one_more_step = false; 41 | return; 42 | } 43 | this.one_more_step = true; 44 | clearTimeout(this.confirm_timeout_id); 45 | this.confirm_timeout_id = setTimeout(() => { 46 | this.one_more_step = false; 47 | }, animation_popup_duration()) 48 | }) 49 | } 50 | } 51 | 52 | export default linysTimeoutButton -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysConfirmDenyButtons.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import linysCapsuleButton from './linysCapsuleButton'; 3 | import linysTimeoutButton from './linysTimeoutButton'; 4 | 5 | // @Reusable 6 | @Component 7 | struct linysConfirmDenyButtons { 8 | onConfirm?: () => void; 9 | onDeny?: () => void; 10 | confirm_double_confirm: boolean = false; 11 | deny_double_confirm: boolean = false; 12 | confirm_text: ResourceStr = ' 󰀓 '; 13 | deny_text: ResourceStr = ' 󰁖 ' 14 | @Prop confirm_enabled: boolean = true; 15 | @Prop deny_enabled: boolean = true; 16 | // Settings / Accessibility 17 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 18 | 19 | build() { 20 | Row({ space: 10 }) { 21 | if (this.onDeny) { 22 | if (this.deny_double_confirm) { 23 | linysTimeoutButton({ 24 | text: this.deny_text, onExecution: () => { 25 | if (this.onDeny) { 26 | this.onDeny(); 27 | } 28 | } 29 | }) 30 | } else { 31 | linysCapsuleButton({ text: this.deny_text }) // Deny 32 | .onClick(() => { 33 | if (this.onDeny) { 34 | this.onDeny(); 35 | } 36 | }) 37 | .enabled(this.deny_enabled) 38 | .opacity(this.deny_enabled ? 1 : 0.7) 39 | } 40 | } 41 | 42 | if (this.onConfirm) { 43 | if (this.confirm_double_confirm) { 44 | linysTimeoutButton({ 45 | text: this.confirm_text, onExecution: () => { 46 | if (this.onConfirm) { 47 | this.onConfirm(); 48 | } 49 | } 50 | }) 51 | } else { 52 | linysCapsuleButton({ text: this.confirm_text }) // Submit 53 | .onClick(() => { 54 | if (this.onConfirm) { 55 | this.onConfirm(); 56 | } 57 | }) 58 | .enabled(this.confirm_enabled) 59 | .opacity(this.confirm_enabled ? 1 : 0.7) 60 | } 61 | } 62 | } 63 | .justifyContent(this.preferred_hand_left_or_right == 'right' ? FlexAlign.End : FlexAlign.Start) 64 | .animation(animation_default()) 65 | .width('100%') 66 | } 67 | } 68 | 69 | export default linysConfirmDenyButtons; -------------------------------------------------------------------------------- /home/src/main/ets/components/buttons/linysShowButton.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, click_effect_default, fontSize_Large } from '../../hosts/bunch_of_defaults'; 2 | import linysSymbol from '../texts/linysSymbol'; 3 | 4 | // @Reusable // Is this OK? 5 | @Component 6 | struct linysShowButton { 7 | // Colors 8 | @StorageLink('effects') effects: boolean = false; 9 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 10 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 11 | @StorageLink('color_current_back') color_current_back: ResourceColor = $r('app.color.font_color_title'); 12 | // Statuses 13 | @Prop show: boolean = false; 14 | @Prop symbol_glyph_target: string = "sys.symbol.square_grid_2x2"; 15 | @Prop color_false: ResourceColor = ""; 16 | @Prop color_true: ResourceColor = ""; 17 | @Prop text: ResourceStr = $r('app.string.Index_tabs_title'); 18 | // UI 19 | show_margin: Padding | number = { left: 5, right: 5 } 20 | @State isHov: boolean = false; 21 | 22 | build() { 23 | Row() { 24 | linysSymbol({ 25 | symbol_glyph_target: this.symbol_glyph_target, 26 | color: this.show 27 | ? ((this.color_true == "") && !this.effects ? this.color_current_primary : this.color_current_font) 28 | : (this.color_false == "" ? this.color_current_font : this.color_false) 29 | }) 30 | 31 | Text(this.text) 32 | .fontSize(fontSize_Large() - 2) 33 | .fontColor(this.effects ? this.color_current_font : this.color_current_primary) 34 | .margin({ left: 4, right: 3 }) 35 | .visibility(this.show ? Visibility.Visible : Visibility.None) 36 | .animation(animation_default()) 37 | } 38 | .brightness(this.isHov ? 1.1 : 1) 39 | .padding(this.show ? 5 : 4) 40 | .margin(this.show ? this.show_margin : 0) 41 | .backgroundColor(this.show ? (this.color_false == "" ? this.color_current_back : this.color_false) : "transparent") 42 | .animation(animation_default()) 43 | .onHover((isHv) => { 44 | this.isHov = isHv; 45 | }) 46 | .clip(true) 47 | .borderRadius(10) 48 | .clickEffect(click_effect_default()) 49 | .accessibilityText(this.text as string) 50 | .accessibilityDescription($r('app.string.Accessibility_desc_linysShowButton')) 51 | .accessibilityGroup(true) 52 | } 53 | } 54 | 55 | export default linysShowButton; -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/managers/ManageListElement.ets: -------------------------------------------------------------------------------- 1 | import linysText from '../../components/texts/linysText'; 2 | import { animation_default, click_effect_default } from '../../hosts/bunch_of_defaults'; 3 | 4 | @Component 5 | struct ManageListElement { 6 | @State text: string = "huawei.com"; 7 | @Prop index: number; 8 | @Link selected: boolean[]; 9 | @Link selected_number: number; 10 | @Prop selecting: boolean; 11 | // Color 12 | @StorageLink('effects') effects: boolean = false; 13 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 14 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 15 | // UI 16 | @State isHov: boolean = false; 17 | 18 | build() { 19 | Column() { 20 | linysText({ 21 | color: this.selected[this.index] && !this.effects ? this.color_current_secondary : this.color_current_font, 22 | text: this.text, 23 | font_weight: this.selected[this.index] ? FontWeight.Bold : FontWeight.Regular 24 | }) 25 | } 26 | .backgroundColor(this.selected[this.index] ? (this.effects ? this.color_current_secondary : this.color_current_font) : 'transparent') 27 | .brightness(this.isHov ? 1.1 : 1) 28 | .animation(animation_default()) 29 | .alignItems(HorizontalAlign.Start) 30 | .width("100%") 31 | .padding({ 32 | left: 10, 33 | right: 10, 34 | top: 5, 35 | bottom: 5 36 | }) 37 | .clickEffect(click_effect_default()) 38 | .onClick(() => { 39 | if (this.selecting) { 40 | // Select 41 | this.select_unselect(); 42 | } 43 | }) 44 | .width("100%") 45 | .gesture( 46 | LongPressGesture({ repeat: false }) 47 | .onAction(() => { 48 | this.select_unselect(); 49 | }) 50 | ) 51 | .onMouse((e) => { 52 | if (e.button == MouseButton.Right && e.action == MouseAction.Press) { 53 | // Right click 54 | this.select_unselect(); 55 | } 56 | }) 57 | .onHover((isHv) => { 58 | this.isHov = isHv; 59 | }) 60 | } 61 | 62 | /** 63 | * Toggles the state 64 | * */ 65 | select_unselect() { 66 | if (this.selected[this.index]) { 67 | this.selected_number -= 1; 68 | } else { 69 | this.selected_number += 1; 70 | } 71 | this.selected[this.index] = !this.selected[this.index]; 72 | } 73 | } 74 | 75 | export default ManageListElement; -------------------------------------------------------------------------------- /home/src/main/ets/objects/CatstensionBridgeObj.ets: -------------------------------------------------------------------------------- 1 | import { bunch_of_extensions, extension } from '../hosts/bunch_of_extensions'; 2 | import { get_message_from_messages } from '../processes/extension_actions'; 3 | 4 | export class CatsBridge { 5 | id: string; 6 | ext: extension; 7 | 8 | constructor(ext: extension) { 9 | this.id = ext.id; 10 | this.ext = ext; 11 | console.warn(this.log_head('Constructed!')); 12 | } 13 | 14 | log_head(text: string) { 15 | const head = `[Extension][${this.ext.name}][${this.ext.id}][CatsBridge]`; 16 | if (text.substring(0, 1) == '[') { 17 | return head + text; 18 | } else { 19 | return `${head} ` + text; 20 | } 21 | } 22 | 23 | chrome_action_setTitle(title: string) { 24 | this.ext.action_default_title = title; 25 | (AppStorage.get('bunch_of_extensions') as bunch_of_extensions).update_timestamp(); 26 | } 27 | 28 | chrome_action_getTitle() { 29 | return this.ext.action_default_title; 30 | } 31 | 32 | chrome_action_getPopup() { 33 | return this.ext.action_default_popup; 34 | } 35 | 36 | chrome_action_setPopup(popup: string, tabId?: number) { 37 | if (popup == '') { 38 | this.ext.action_default_popup = undefined; 39 | try { 40 | this.ext.controller.loadUrl(this.ext.path_full + '/' + this.ext.action_default_popup); 41 | } catch (e) { 42 | console.error(this.log_head('[chrome_action_setPopup] loadUrl Failed: ' + e)); 43 | } 44 | } else { 45 | this.ext.action_default_popup = popup; 46 | } 47 | } 48 | 49 | chrome_i18n_getAcceptLanguages() { 50 | return ['en', 'zh_CN']; 51 | } 52 | 53 | chrome_i18n_getUILanguage() { 54 | return 'en'; 55 | } 56 | 57 | chrome_i18n_getMessage(messageName: string): string | undefined { 58 | if (this.ext.messages) { 59 | return get_message_from_messages(this.ext.messages, messageName); 60 | } else { 61 | return undefined; 62 | } 63 | } 64 | 65 | /** 66 | * Test method. 67 | * @param data Anything to be logged. 68 | * */ 69 | liny_test(data: string) { 70 | console.log(`[Extension][CatsBridge][arkTs] Test! data=[${data}]!`); 71 | } 72 | } 73 | 74 | export function CatsBridgeMethods() { 75 | return ['constructor', 'chrome_action_setTitle', 'chrome_action_getTitle', 'chrome_action_getPopup', 'chrome_action_setPopup', 'chrome_i18n_getAcceptLanguages', 76 | 'chrome_i18n_getUILanguage', 'chrome_i18n_getMessage', 77 | 'liny_test']; 78 | } -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/commands.js: -------------------------------------------------------------------------------- 1 | // Currently empty api. 2 | // TODO: Finish commands.js 3 | 4 | /** 5 | * A standalone class that mirrors the features of the chrome.commands API. 6 | * The internal logic for interacting with the operating system or browser is omitted. 7 | */ 8 | class cathrome_commands { 9 | static Command = class { 10 | constructor(options = {}) { 11 | this.description = options.description; 12 | this.name = options.name; 13 | this.shortcut = options.shortcut; 14 | } 15 | }; 16 | 17 | constructor() { 18 | /** 19 | * Fired when a registered command is activated using a keyboard shortcut. 20 | * The callback is expected to receive `(command: string, tab?: object) => void`. 21 | * @type {cathrome_eventListener} 22 | */ 23 | this.onCommand = new cathrome_eventListener(); 24 | 25 | // Placeholder for internal state, mimicking registered commands. 26 | /** @private @type {Command[]} */ 27 | this._registeredCommands = [ 28 | // { 29 | // name: "example-command-1", 30 | // description: "Toggles a feature.", 31 | // shortcut: "Ctrl+Shift+1" 32 | // }, 33 | // { 34 | // name: "_execute_action", 35 | // description: "Reserved for the extension's primary action.", 36 | // shortcut: "Ctrl+U" 37 | // } 38 | ]; 39 | } 40 | 41 | fire(command, tab) { 42 | this.onCommand.fire(command, tab); 43 | } 44 | 45 | /** 46 | * Returns all the registered extension commands for this extension and their shortcut (if active). 47 | * 48 | * In a real environment, this would involve async browser communication. 49 | * Here, we use a callback to mimic the API's structure and immediately return placeholder data. 50 | * 51 | * @param {function(Command[]): void} callback A function that is called with an array of Command objects. 52 | * @returns {void} 53 | */ 54 | getAll(callback) { 55 | if (typeof callback === 'function') { 56 | // Return a copy of the placeholder data to the callback. 57 | callback(this._registeredCommands.slice()); 58 | } 59 | // NOTE: The real API supports a Promise return style as well, but we stick to the 60 | // common callback pattern for simplicity in this standalone structure. 61 | } 62 | } -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/prompts/woofPromptOK.ets: -------------------------------------------------------------------------------- 1 | import linysCapsuleButton from '../../components/buttons/linysCapsuleButton'; 2 | import linysText from '../../components/texts/linysText'; 3 | import { animation_default } from '../../hosts/bunch_of_defaults'; 4 | import lazy woofControlFrame from '../woofControlFrame'; 5 | 6 | @CustomDialog 7 | struct woofPromptOK { 8 | controller: CustomDialogController; 9 | @Prop title: ResourceStr = $r('app.string.Settings_toolbox_import_settings') 10 | @Prop desc: ResourceStr = $r('app.string.Settings_toolbox_import_settings_ok') 11 | // Colors 12 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 13 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 14 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 15 | // Status 16 | @State content_height: number = 233; 17 | // Settings / Accessibility 18 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 19 | // Count 20 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 21 | 22 | aboutToAppear(): void { 23 | this.opened_dialog_controllers.push(this.controller); 24 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 25 | } 26 | 27 | aboutToDisappear(): void { 28 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 29 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 30 | } 31 | 32 | build() { 33 | woofControlFrame({ 34 | title: this.title, 35 | controller: this.controller 36 | }) { 37 | Scroll() { 38 | Column({ space: 15 }) { 39 | linysText({ 40 | text: this.desc, 41 | max_lines: 5 42 | }) 43 | .width("100%") 44 | linysCapsuleButton({ text: $r('app.string.OK') }) 45 | .onClick(()=>{ 46 | if (this.controller) { 47 | this.controller.close(); 48 | } 49 | }) 50 | } 51 | .alignItems(this.preferred_hand_left_or_right == 'right' ? HorizontalAlign.End : HorizontalAlign.Start) 52 | .animation(animation_default()) 53 | .justifyContent(FlexAlign.Center) 54 | .width("100%") 55 | .onAreaChange((_o, n) => { 56 | this.content_height = n.height as number; 57 | }) 58 | } 59 | .layoutWeight(1) 60 | .edgeEffect(EdgeEffect.Spring) 61 | .constraintSize({ maxHeight: this.content_height }) 62 | } 63 | } 64 | } 65 | 66 | export default woofPromptOK; -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/web/woofConfirm.ets: -------------------------------------------------------------------------------- 1 | import linysConfirmDenyButtons from '../../components/buttons/linysConfirmDenyButtons'; 2 | import linysText from '../../components/texts/linysText'; 3 | import linysTextArea from '../../components/texts/linysTextArea'; 4 | import linysTextTitle from '../../components/texts/linysTextTitle'; 5 | 6 | @CustomDialog 7 | struct woofConfirm { 8 | // @Link event: OnConfirmEvent | undefined; 9 | controller: CustomDialogController; 10 | @LocalStorageLink('screen_height') screen_height: number = 0; 11 | // Colors 12 | @StorageLink('effects') effects: boolean = false; 13 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 14 | // Count 15 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 16 | // Actions 17 | title: ResourceStr = ''; 18 | desc: string = ''; 19 | @State desc_edit: string = ''; // To fool the TextArea 20 | onConfirm?: () => void; 21 | onDeny?: () => void; 22 | 23 | aboutToAppear(): void { 24 | this.opened_dialog_controllers.push(this.controller); 25 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 26 | } 27 | 28 | aboutToDisappear(): void { 29 | if (this.onDeny) { 30 | this.onDeny(); 31 | } 32 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 33 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 34 | } 35 | 36 | build() { 37 | Column({ space: 15 }) { 38 | linysTextTitle({ text: this.title, max_lines: 2 }) // Page url 39 | .width("100%") 40 | 41 | linysTextArea({ 42 | init_text:this.desc, 43 | text: this.desc_edit 44 | }) 45 | .constraintSize({ maxHeight: Math.max(0, this.screen_height - 160) }) 46 | 47 | linysConfirmDenyButtons({ 48 | onConfirm: () => { 49 | if (this.onConfirm) { 50 | this.onConfirm(); 51 | } 52 | if (this.controller) { 53 | this.controller.close(); 54 | } 55 | }, 56 | onDeny: () => { 57 | if (this.onDeny) { 58 | this.onDeny(); 59 | } 60 | if (this.controller) { 61 | this.controller.close(); 62 | } 63 | }, 64 | deny_double_confirm: true, 65 | confirm_double_confirm: true, 66 | }) // Buttons 67 | } 68 | .padding(20) 69 | .alignItems(HorizontalAlign.Start) 70 | .justifyContent(FlexAlign.Center) 71 | .width("100%") 72 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 73 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 74 | } 75 | } 76 | 77 | export default woofConfirm; -------------------------------------------------------------------------------- /home/src/main/ets/blocks/panels/meowAnimationManager.ets: -------------------------------------------------------------------------------- 1 | import { bunch_of_settings } from '../../hosts/bunch_of_settings'; 2 | import linysLockSlider from '../../components/linysLockSlider'; 3 | import linysText from '../../components/texts/linysText'; 4 | import linysTimeoutButtonWithText from '../../components/buttons/linysTimeoutButtonWithText'; 5 | 6 | @Component 7 | struct meowAnimationManager { 8 | // Colors 9 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 10 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 11 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 12 | // editing 13 | @StorageLink('animation_response') animation_response: number = 36; 14 | @StorageLink('animation_damping_coefficient') animation_damping_coefficient: number = 20; 15 | 16 | build() { 17 | Column({ space: 10 }) { 18 | 19 | linysText({ 20 | text: $r('app.string.Settings_appearance_animation_response') 21 | }) // Slider and display of animation_response 22 | 23 | linysLockSlider({ 24 | slider_min: 1, 25 | slider_max: 100, 26 | display: (0.01 * this.animation_response).toFixed(2), 27 | slider_value: this.animation_response, 28 | onTouchUp: () => { 29 | bunch_of_settings.set('animation_response', this.animation_response); 30 | } 31 | }) 32 | 33 | linysText({ 34 | text: $r('app.string.Settings_appearance_animation_damping_coefficient') 35 | }) // Slider and display of animation damping coefficient 36 | 37 | linysLockSlider({ 38 | slider_min: 0, 39 | slider_max: 75, 40 | slider_value: this.animation_damping_coefficient, 41 | display: (0.01 * this.animation_damping_coefficient).toFixed(2), 42 | onTouchUp: () => { 43 | bunch_of_settings.set('animation_damping_coefficient', 44 | this.animation_damping_coefficient); 45 | } 46 | }) 47 | 48 | linysTimeoutButtonWithText({ 49 | desc_text: $r('app.string.Settings_appearance_animation_reset'), 50 | button_text: ' 󰃈 ', 51 | onExecution: () => { 52 | // Reset 53 | bunch_of_settings.reset('animation_response'); 54 | bunch_of_settings.reset('animation_damping_coefficient'); 55 | // Sync 56 | this.animation_response = bunch_of_settings.get('animation_response') as number; 57 | console.log(this.animation_response.toString()) 58 | this.animation_damping_coefficient = bunch_of_settings.get('animation_damping_coefficient') as number; 59 | } 60 | }) 61 | } 62 | .alignItems(HorizontalAlign.Start) 63 | .justifyContent(FlexAlign.Start) 64 | } 65 | } 66 | 67 | export default meowAnimationManager; -------------------------------------------------------------------------------- /home/src/main/ets/components/texts/linysText.ets: -------------------------------------------------------------------------------- 1 | import lazy { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import { click_effect_default, fontSize_Normal } from '../../hosts/bunch_of_defaults'; 3 | import { LengthMetrics } from '@kit.ArkUI'; 4 | 5 | // @Reusable // Will this affect anything? 6 | @Component 7 | struct linysText { 8 | // Colors 9 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 10 | @Prop text: ResourceStr = $r('app.string.HomeAbility_label'); 11 | @Prop color: ResourceColor = ""; 12 | @Prop font_weight: FontWeight = FontWeight.Regular; 13 | is_description: boolean = false; 14 | max_lines: number = 1; 15 | overflow_type: TextOverflow = TextOverflow.Ellipsis; 16 | // UI 17 | @Prop is_expanded: boolean = false; 18 | can_hover: boolean = false; 19 | @State isHov: boolean = false; 20 | 21 | build() { 22 | if (this.is_description) { 23 | Scroll() { 24 | Text(this.text) 25 | .fontColor(this.color == "" ? this.color_current_font : this.color) 26 | .fontSize(fontSize_Normal()) 27 | .fontWeight(this.font_weight) 28 | .maxLines(!this.is_expanded ? 1 : undefined) 29 | .lineSpacing(LengthMetrics.vp(4)) 30 | .textOverflow({ overflow: this.overflow_type }) 31 | .animation(animation_default()) 32 | } 33 | .nestedScroll({ scrollForward: NestedScrollMode.PARENT_FIRST, scrollBackward: NestedScrollMode.PARENT_FIRST }) 34 | .align(Alignment.TopStart) 35 | .scrollable(ScrollDirection.Vertical) 36 | .scrollBar(BarState.Off) 37 | // .edgeEffect(EdgeEffect.Spring) 38 | .constraintSize(this.is_expanded ? {} : { maxHeight: 40 }) 39 | .animation(animation_default()) 40 | .borderRadius(12) 41 | .padding(10) 42 | .backgroundColor($r('sys.color.comp_background_tertiary')) 43 | .width('100%') 44 | .onClick(() => { 45 | this.is_expanded = !this.is_expanded; 46 | }) 47 | .clickEffect(click_effect_default()) 48 | .hoverEffect(HoverEffect.Highlight) 49 | 50 | } else { 51 | Text(this.text) 52 | .brightness(this.isHov ? 1.1 : 1) 53 | .fontWeight(this.font_weight) 54 | .animation(this.can_hover ? animation_default() : { duration: 0 }) 55 | .padding(this.can_hover ? { 56 | left: 7, 57 | right: 7, 58 | top: 5, 59 | bottom: 5 60 | } : 0) 61 | .borderRadius(this.can_hover ? 7.5 : 0) 62 | .fontColor(this.color == "" ? this.color_current_font : this.color) 63 | .fontSize(fontSize_Normal()) 64 | .maxLines(this.max_lines) 65 | .lineSpacing(LengthMetrics.vp(4)) 66 | .textOverflow({ overflow: this.overflow_type }) 67 | .onHover((isHv) => { 68 | if (this.can_hover) { 69 | this.isHov = isHv; 70 | } 71 | }) 72 | } 73 | } 74 | } 75 | 76 | export default linysText; -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/prompts/woofWantJump.ets: -------------------------------------------------------------------------------- 1 | import linysConfirmDenyButtons from '../../components/buttons/linysConfirmDenyButtons'; 2 | import linysText from '../../components/texts/linysText'; 3 | import linysTextArea from '../../components/texts/linysTextArea'; 4 | import linysTextTitle from '../../components/texts/linysTextTitle'; 5 | import { jump_external_link } from '../../utils/url_tools'; 6 | 7 | @CustomDialog 8 | struct woofWantJump { 9 | controller: CustomDialogController; 10 | @Prop link: string = ''; 11 | // Colors 12 | @StorageLink('effects') effects: boolean = false; 13 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 14 | // UI display 15 | @LocalStorageLink('screen_height') screen_height: number = 0; 16 | @State fixed_height_title: number = 0; 17 | @State fixed_height_quest: number = 0; 18 | // Count 19 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 20 | 21 | aboutToAppear(): void { 22 | this.opened_dialog_controllers.push(this.controller); 23 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 24 | } 25 | 26 | aboutToDisappear(): void { 27 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 28 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 29 | } 30 | 31 | build() { 32 | Column({ space: 15 }) { 33 | linysTextTitle({ text: $r('app.string.LinkJumper_webpage_asks_for_jump') })// This page is asking for a jump 34 | .width("100%") 35 | .onAreaChange((o, n) => { 36 | this.fixed_height_title = n.height as number; 37 | }) 38 | 39 | linysTextArea({ init_text: this.link, text: this.link }) // Edit 40 | .constraintSize({ 41 | maxHeight: Math.max(0, this.screen_height * 0.9 - this.fixed_height_quest - this.fixed_height_title - 35 - 45 - 40) 42 | }) 43 | 44 | linysText({ text: $r('app.string.LinkJumper_allow_jump') })// allow? 45 | .width("100%") 46 | .onAreaChange((o, n) => { 47 | this.fixed_height_quest = n.height as number; 48 | }) 49 | 50 | linysConfirmDenyButtons({ 51 | onConfirm: () => { 52 | if (this.controller) { 53 | this.controller.close(); 54 | } 55 | jump_external_link(this.link, this.getUIContext().getHostContext()!); 56 | }, 57 | onDeny: () => { 58 | if (this.controller) { 59 | this.controller.close(); 60 | } 61 | } 62 | }) // Buttons 63 | } 64 | .padding(20) 65 | .alignItems(HorizontalAlign.Start) 66 | .justifyContent(FlexAlign.Center) 67 | .width("100%") 68 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 69 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 70 | } 71 | } 72 | 73 | export default woofWantJump; -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/contents/woofQR.ets: -------------------------------------------------------------------------------- 1 | import linysText from '../../components/texts/linysText'; 2 | import linysTextTitle from '../../components/texts/linysTextTitle'; 3 | import { click_effect_default } from '../../hosts/bunch_of_defaults'; 4 | import { copy } from '../../utils/clipboard_tools'; 5 | import lazy woofControlFrame from '../woofControlFrame'; 6 | 7 | @CustomDialog 8 | struct woofQR { 9 | controller: CustomDialogController; 10 | @Prop link: string = ''; 11 | @Prop title: string = ''; 12 | // Colors 13 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 14 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 15 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 16 | // Status 17 | @State content_height: number = 233; 18 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 19 | 20 | aboutToAppear(): void { 21 | this.opened_dialog_controllers.push(this.controller); 22 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 23 | } 24 | 25 | aboutToDisappear(): void { 26 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 27 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 28 | } 29 | 30 | build() { 31 | woofControlFrame({ 32 | title: $r('app.string.QR_code'), 33 | controller: this.controller 34 | }) { 35 | Scroll() { 36 | Column({ space: 10 }) { 37 | QRCode(this.link) 38 | .color(this.color_current_font) 39 | .backgroundColor("transparent") 40 | .padding(20) 41 | 42 | Column() { 43 | linysTextTitle({ 44 | text: this.title, 45 | max_lines: 5, 46 | can_hover: true 47 | }) 48 | .width("100%") 49 | .clickEffect(click_effect_default()) 50 | .onClick(() => { 51 | copy(this.title); 52 | }) 53 | 54 | linysText({ 55 | text: this.link, 56 | font_weight: FontWeight.Bold, 57 | max_lines: 10, 58 | can_hover: true 59 | }) 60 | .width("100%") 61 | .opacity(0.7) 62 | .clickEffect(click_effect_default()) 63 | .onClick(() => { 64 | copy(this.link); 65 | }) 66 | } // Info 67 | 68 | } 69 | .alignItems(HorizontalAlign.Center) 70 | .justifyContent(FlexAlign.Center) 71 | .width("100%") 72 | .onAreaChange((_o, n) => { 73 | this.content_height = n.height as number; 74 | }) 75 | } 76 | .layoutWeight(1) 77 | .edgeEffect(EdgeEffect.Spring) 78 | .constraintSize({ maxHeight: this.content_height }) 79 | } 80 | } 81 | } 82 | 83 | export default woofQR; -------------------------------------------------------------------------------- /home/src/main/ets/blocks/modules/meowExtensionView.ets: -------------------------------------------------------------------------------- 1 | import { animation_default } from '../../hosts/bunch_of_defaults'; 2 | import { bunch_of_extensions } from '../../hosts/bunch_of_extensions'; 3 | import { createEWeb, ExtNodeController } from '../../objects/ExtWebNode'; 4 | import { detach_show_extension } from '../../processes/web_actions'; 5 | 6 | @Component 7 | struct meowExtensionView { 8 | @StorageLink('bunch_of_extensions') bunch_of_extensions: bunch_of_extensions = new bunch_of_extensions(); 9 | // UI 10 | @LocalStorageLink('tablet_mode') tablet_mode: boolean = false; 11 | @LocalStorageLink('compact_mode') compact_mode: boolean = false; // Like PuraX 12 | @StorageLink('effects') effects: boolean = false; 13 | @LocalStorageLink('my_window_id') my_window_id: string = ''; 14 | // Ext 15 | @LocalStorageLink('showing_ext_id') showing_ext_id: string = ''; 16 | @LocalStorageLink('showing_ext_node_controller') showing_ext_node_controller: ExtNodeController | undefined = undefined; 17 | 18 | aboutToAppear(): void { 19 | // Init web to create the ExtWeb Nodes for extensions loaded from disk. 20 | if (this.bunch_of_extensions.NodeControllers.length == 0) { 21 | for (let index = 0; index < this.bunch_of_extensions.extensions.length; index++) { 22 | this.bunch_of_extensions.NodeControllers.push(new ExtNodeController(createEWeb(this.getUIContext(), this.bunch_of_extensions.extensions[index]))); 23 | } 24 | console.log(`[meowExtensionView] bunch_of_extensions.extensions.lengtgh = ${this.bunch_of_extensions.extensions.length}. Created NodeControllers!`); 25 | } else { 26 | console.log(`[meowExtensionView] I'm a sub window. I found there are already created NodeControllers. I have nothing to do. Oh sad sad...`); 27 | } 28 | } 29 | 30 | build() { 31 | // if (this.showing_ext_node_controller) { 32 | Stack() { 33 | Row() 34 | .width('100%') 35 | .height('100%') 36 | .backgroundColor($r('sys.color.comp_background_tertiary')) 37 | .onClick(() => { 38 | let ext = this.bunch_of_extensions.get_extension(this.showing_ext_id); 39 | // if (ext) { 40 | // ext.is_being_viewed = false; 41 | // } 42 | // this.showing_ext_id = ''; 43 | if (ext) { 44 | detach_show_extension(ext); 45 | } else { 46 | this.showing_ext_node_controller = undefined; 47 | this.showing_ext_id = ''; 48 | } 49 | }) 50 | 51 | if (this.showing_ext_node_controller) { 52 | NodeContainer(this.showing_ext_node_controller) 53 | .width(this.tablet_mode ? '60%' : undefined) 54 | .height('70%') 55 | .animation(animation_default()) 56 | .margin(12) 57 | .clip(true) 58 | .borderRadius(this.effects ? 5 : 0) 59 | .onAppear(() => { 60 | console.log(`[meowExtensionView][${this.my_window_id}] Hello NodeContainer.`); 61 | }) 62 | } else { 63 | Row().onAppear(() => { 64 | console.log(`[meowExtensionView][${this.my_window_id}] Goodbye NodeContainer.`); 65 | }) 66 | } 67 | 68 | } 69 | .alignContent(Alignment.TopEnd) 70 | .visibility((this.showing_ext_id != '') ? Visibility.Visible : Visibility.Hidden) 71 | .animation(animation_default()) 72 | .width('100%') 73 | .height('100%') 74 | .hitTestBehavior((this.showing_ext_id != '') ? HitTestMode.Default : HitTestMode.Transparent) 75 | } 76 | // } 77 | } 78 | 79 | export default meowExtensionView; -------------------------------------------------------------------------------- /home/src/main/ets/objects/HistoryDataSource.ets: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rendering-control-lazyforeach 3 | * */ 4 | import { history_record } from '../hosts/bunch_of_history'; 5 | 6 | // BasicDataSource实现了IDataSource接口,用于管理listener监听,以及通知LazyForEach数据更新 7 | class BasicHistoryDataSource implements IDataSource { 8 | private listeners: DataChangeListener[] = []; 9 | private originDataArray: Array> = []; 10 | 11 | public totalCount(): number { 12 | return 0; 13 | } 14 | 15 | public getData(index: number): Array { 16 | return this.originDataArray[index]; 17 | } 18 | 19 | // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听 20 | registerDataChangeListener(listener: DataChangeListener): void { 21 | if (this.listeners.indexOf(listener) < 0) { 22 | // console.info('add listener'); 23 | this.listeners.push(listener); 24 | } 25 | } 26 | 27 | // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听 28 | unregisterDataChangeListener(listener: DataChangeListener): void { 29 | const pos = this.listeners.indexOf(listener); 30 | if (pos >= 0) { 31 | // console.info('remove listener'); 32 | this.listeners.splice(pos, 1); 33 | } 34 | } 35 | 36 | // 通知LazyForEach组件需要重载所有子组件 37 | notifyDataReload(): void { 38 | this.listeners.forEach(listener => { 39 | listener.onDataReloaded(); 40 | }); 41 | } 42 | 43 | // 通知LazyForEach组件需要在index对应索引处添加子组件 44 | notifyDataAdd(index: number): void { 45 | this.listeners.forEach(listener => { 46 | listener.onDataAdd(index); 47 | // 写法2:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]); 48 | }); 49 | } 50 | 51 | // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件 52 | notifyDataChange(index: number): void { 53 | this.listeners.forEach(listener => { 54 | listener.onDataChange(index); 55 | // 写法2:listener.onDatasetChange([{type: DataOperationType.CHANGE, index: index}]); 56 | }); 57 | } 58 | 59 | // 通知LazyForEach组件需要在index对应索引处删除该子组件 60 | notifyDataDelete(index: number): void { 61 | this.listeners.forEach(listener => { 62 | listener.onDataDelete(index); 63 | // 写法2:listener.onDatasetChange([{type: DataOperationType.DELETE, index: index}]); 64 | }); 65 | } 66 | 67 | // 通知LazyForEach组件将from索引和to索引处的子组件进行交换 68 | notifyDataMove(from: number, to: number): void { 69 | this.listeners.forEach(listener => { 70 | listener.onDataMove(from, to); 71 | // 写法2:listener.onDatasetChange( 72 | // [{type: DataOperationType.EXCHANGE, index: {start: from, end: to}}]); 73 | }); 74 | } 75 | 76 | notifyDatasetChange(operations: DataOperation[]): void { 77 | this.listeners.forEach(listener => { 78 | listener.onDatasetChange(operations); 79 | }); 80 | } 81 | } 82 | 83 | export default class HistoryDataSource extends BasicHistoryDataSource { 84 | private dataArray: Array> = []; 85 | 86 | constructor(fromData: Array>) { 87 | super(); 88 | this.dataArray = fromData; 89 | } 90 | 91 | public setData(data: Array>): void { 92 | this.dataArray = data; 93 | this.notifyDataReload(); 94 | } 95 | 96 | public totalCount(): number { 97 | return this.dataArray.length; 98 | } 99 | 100 | public getData(index: number): Array { 101 | return this.dataArray[index]; 102 | } 103 | 104 | public pushData(data: Array): void { 105 | this.dataArray.push(data); 106 | this.notifyDataAdd(this.dataArray.length - 1); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/prompts/woofWantProtectedResources.ets: -------------------------------------------------------------------------------- 1 | import linysConfirmDenyButtons from '../../components/buttons/linysConfirmDenyButtons'; 2 | import linysText from '../../components/texts/linysText'; 3 | import linysTextTitle from '../../components/texts/linysTextTitle'; 4 | import { click_effect_default } from '../../hosts/bunch_of_defaults'; 5 | import { desc_for_ProtectedResourceTypes, permissions_for_ProtectedResourceTypes, request_user_permission } from '../../utils/permission_tools'; 6 | import { common } from '@kit.AbilityKit'; 7 | 8 | @CustomDialog 9 | struct woofWantResources { 10 | controller: CustomDialogController; 11 | @Prop protected_resource_types: string[] = []; 12 | @Prop source: string = 'source'; 13 | onPermit?: () => void; 14 | onDeny?: () => void; 15 | // Colors 16 | @StorageLink('effects') effects: boolean = false; 17 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 18 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 19 | // Settings / Accessibility 20 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 21 | // UI display 22 | @LocalStorageLink('screen_height') screen_height: number = 0; 23 | // Count 24 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 25 | 26 | aboutToAppear(): void { 27 | this.opened_dialog_controllers.push(this.controller); 28 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 29 | } 30 | 31 | aboutToDisappear(): void { 32 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 33 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 34 | } 35 | 36 | build() { 37 | Column({ space: 15 }) { 38 | linysTextTitle({ text: $r('app.string.Permission_Manager_webpage_asks_for_permission'), max_lines: 3 })// This page is asking for a jump 39 | .width("100%") 40 | linysText({ text: this.source }) 41 | 42 | Column({ space: 10 }) { 43 | ForEach(desc_for_ProtectedResourceTypes(this.protected_resource_types), ((desc: ResourceStr) => { 44 | linysText({ 45 | text: desc, 46 | max_lines: 5 47 | }) 48 | .padding(10) 49 | .borderRadius(13.5) 50 | .clickEffect(click_effect_default()) 51 | .backgroundColor(this.color_current_secondary) 52 | })) 53 | } 54 | .alignItems(HorizontalAlign.Start) 55 | 56 | linysText({ text: $r('app.string.Permission_Manager_grant_permission') })// allow? 57 | .width("100%") 58 | 59 | linysConfirmDenyButtons({ 60 | onConfirm: () => { 61 | request_user_permission(this.getUIContext().getHostContext() as common.UIAbilityContext, 62 | permissions_for_ProtectedResourceTypes(this.protected_resource_types)); 63 | if (this.onPermit) { 64 | this.onPermit(); 65 | } 66 | if (this.controller) { 67 | this.controller.close(); 68 | } 69 | }, 70 | onDeny: () => { 71 | if (this.onDeny) { 72 | this.onDeny(); 73 | } 74 | if (this.controller) { 75 | this.controller.close(); 76 | } 77 | } 78 | }) // Buttons 79 | } 80 | .padding(20) 81 | .alignItems(HorizontalAlign.Start) 82 | .justifyContent(FlexAlign.Center) 83 | .width("100%") 84 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 85 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 86 | } 87 | } 88 | 89 | export default woofWantResources; -------------------------------------------------------------------------------- /home/src/main/ets/hosts/bunch_of_user_agents.ets: -------------------------------------------------------------------------------- 1 | import { bunch_of_settings } from "./bunch_of_settings"; 2 | 3 | export class bunch_of_user_agents { 4 | static list_of_user_agents: user_agent[] = []; 5 | 6 | /** 7 | * Update last accessed time. This would trigger some refresh on UI layer. 8 | * */ 9 | static update_last_accessed() { 10 | AppStorage.setOrCreate('bunch_of_user_agents_update', Date.now()); 11 | } 12 | 13 | /** 14 | * Sets the global UA. 15 | * @param idx The index of UA. 16 | * */ 17 | static set_global_UA(idx: number) { 18 | if (idx == -99) { 19 | // Setting index to -99 then the real value 20 | // Can force refresh @Watch 21 | // And the index -99 has no meaning 22 | return; 23 | } 24 | AppStorage.setOrCreate('user_agent_selected', idx); 25 | if (idx <= -1) { 26 | // If set back to default 27 | AppStorage.setOrCreate('universal_global_custom_ua_gateway', ''); 28 | } else { 29 | AppStorage.setOrCreate('universal_global_custom_ua_gateway', bunch_of_user_agents.list_of_user_agents[idx].user_agent_content); 30 | } 31 | bunch_of_settings.set('custom_user_agents_selected_index', idx); 32 | } 33 | 34 | /** 35 | * Add a new user agent to the current user agent list. 36 | * @param se A user_agent object, the user agent to be added. 37 | * */ 38 | static add_user_agent(ua: user_agent) { 39 | bunch_of_user_agents.list_of_user_agents.push(ua); 40 | bunch_of_user_agents.update_last_accessed(); 41 | } 42 | 43 | /** 44 | * Remove a user agent from the current user agent list. 45 | * @param index A number, the index of the user agent to be removed in the list. 46 | * */ 47 | static del_user_agent(index: number) { 48 | bunch_of_user_agents.list_of_user_agents.splice(index, 1); 49 | bunch_of_user_agents.update_last_accessed(); 50 | } 51 | 52 | /** 53 | * Export user agents in a specific plain text format. 54 | * @returns '\n' connected string of user agents in the format of: 55 | * @example 'Firefox Windows 132\nMozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0' 56 | * */ 57 | static export_string() { 58 | let export_list: string[] = [] 59 | for (let index = 0; index < bunch_of_user_agents.list_of_user_agents.length; index++) { 60 | let ua: user_agent = bunch_of_user_agents.list_of_user_agents[index]; 61 | export_list.push(ua.label); 62 | export_list.push(ua.user_agent_content); 63 | } 64 | return export_list.join("\n"); 65 | } 66 | 67 | /** 68 | * Import user agents in a specific plain text format. 69 | * 70 | * In default overwrites whatever was in this list_of_user_agents. 71 | * @param imp The string in the correct format: 72 | * @example 'Firefox Windows 132\nMozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0' 73 | * */ 74 | static import_string(imp: string) { 75 | // Clear 76 | bunch_of_user_agents.list_of_user_agents = []; 77 | if (imp == "") { 78 | return; 79 | } 80 | // Import 81 | let import_list: string[] = imp.split("\n"); 82 | for (let index = 0; index < import_list.length; index += 2) { 83 | if (import_list[index] != "") { 84 | bunch_of_user_agents.add_user_agent(new user_agent(import_list[index], import_list[index+1])); 85 | } 86 | } 87 | } 88 | } 89 | 90 | export class user_agent { 91 | label: string; 92 | user_agent_content: string; 93 | 94 | /** 95 | * A user_agent object, which consists of a label and the user agent text content. 96 | * @param label The name of the user agent. 97 | * @param user_agent_content The content of the user agent. 98 | */ 99 | constructor(label: string, content: string) { 100 | this.label = label; 101 | this.user_agent_content = content; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /home/src/main/ets/components/linysPathTree.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, click_effect_default } from '../hosts/bunch_of_defaults'; 2 | import linysText from './texts/linysText'; 3 | 4 | @Component 5 | struct linysPathTree { 6 | @Link @Watch('on_current_viewing_path_change') current_viewing_path: string; 7 | @LocalStorageProp('screen_height') screen_height: number = 100; 8 | @Prop max_height_screen_percentage: number = 0.4; 9 | @State path_stack: string[] = []; 10 | @State label_stack: string[] = []; 11 | scroller: Scroller = new Scroller(); 12 | // Settings / Accessibility 13 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 14 | // Colors 15 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 16 | @State isHov: boolean = false; 17 | 18 | build() { 19 | Scroll(this.scroller) { 20 | Column({ space: 5 }) { 21 | Row() { 22 | linysText({ text: " 󰀧 ", font_weight: FontWeight.Bold }); 23 | } 24 | .brightness(this.isHov ? 1.1 : 1) 25 | .backgroundColor(this.color_current_secondary) 26 | .animation(animation_default()) 27 | .borderRadius(10) 28 | .padding(10) 29 | .clickEffect(click_effect_default()) 30 | .onClick(() => { 31 | this.current_viewing_path = ""; 32 | }) 33 | .onHover((isHv) => { 34 | this.isHov = isHv; 35 | }) 36 | 37 | ForEach(this.label_stack, (item: string, index: number) => { 38 | linysPathButton({ 39 | current_viewing_path: this.current_viewing_path, 40 | label: item, 41 | my_directory: this.path_stack[index], 42 | }) 43 | }) 44 | } 45 | .alignItems(this.preferred_hand_left_or_right == 'right' ? HorizontalAlign.End : HorizontalAlign.Start) 46 | .width("100%") 47 | .animation(animation_default()) 48 | } 49 | .padding({ top: 5 }) 50 | .scrollBar(BarState.Off) 51 | .height(5 + 42 + Math.min(this.screen_height * this.max_height_screen_percentage, 45 * this.label_stack.length)) 52 | .animation(animation_default()) 53 | .edgeEffect(EdgeEffect.Spring) 54 | } 55 | 56 | on_current_viewing_path_change() { 57 | this.decompose_path(this.current_viewing_path); 58 | } 59 | 60 | decompose_path(path: string) { 61 | let split_labels = path.split("/"); 62 | let result: string[] = []; 63 | let meow_path = ""; 64 | for (let index = 0; index < split_labels.length; index++) { 65 | let label = split_labels[index]; 66 | if (meow_path == '') { 67 | meow_path = label; 68 | result.push(label); 69 | } else { 70 | meow_path = meow_path + "/" + label; 71 | result.push(meow_path); 72 | } 73 | } 74 | this.path_stack = result; 75 | this.label_stack = split_labels; 76 | if (this.label_stack.join() == "") { 77 | this.label_stack = []; 78 | } 79 | } 80 | } 81 | 82 | export default linysPathTree; 83 | 84 | @Component 85 | struct linysPathButton { 86 | @Prop label: string = "label"; 87 | @Prop my_directory: string = "label"; 88 | @Link current_viewing_path: string; 89 | // Colors 90 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 91 | @State isHov: boolean = false; 92 | 93 | build() { 94 | Row() { 95 | linysText({ text: this.label }); 96 | } 97 | .brightness(this.isHov ? 1.1 : 1) 98 | .backgroundColor(this.color_current_secondary) 99 | .animation(animation_default()) 100 | .borderRadius(10) 101 | .padding(10) 102 | .clickEffect(click_effect_default()) 103 | .onClick(() => { 104 | this.current_viewing_path = this.my_directory; 105 | }) 106 | .onHover((isHv) => { 107 | this.isHov = isHv; 108 | }) 109 | } 110 | } -------------------------------------------------------------------------------- /home/src/main/ets/workers/History_index_saver.ets: -------------------------------------------------------------------------------- 1 | import { collections, ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; 2 | import { bunch_of_history_index } from '../hosts/bunch_of_history_index'; 3 | import { sandbox_save } from '../utils/storage_tools'; 4 | import { fileIo } from '@kit.CoreFileKit'; 5 | import { common } from '@kit.AbilityKit'; 6 | 7 | const workerPort: ThreadWorkerGlobalScope = worker.workerPort; 8 | let map: collections.Map>; 9 | let file_name: string; 10 | let filesDir: string; 11 | let clear: boolean; 12 | /** 13 | * Defines the event handler to be called when the worker thread receives a message sent by the host thread. 14 | * The event handler is executed in the worker thread. 15 | * 16 | * @param event message data 17 | */ 18 | workerPort.onmessage = (event: MessageEvents) => { 19 | if (typeof event.data == "string") { 20 | // receive information from main thread. 21 | const data = event.data.split(":"); 22 | const header = data[0]; 23 | const content = data[1]; 24 | // set options 25 | if (header == 'filesDir') { 26 | filesDir = content; 27 | } else if (header == 'file_name') { 28 | file_name = content; 29 | } else if (header == 'clear') { 30 | clear = content == 'true' ? true : false; 31 | } 32 | } else { 33 | // Receives the map. LETS GOO!~ 34 | map = event.data; 35 | 36 | // Save to disk 37 | save_index_ext(file_name, map, clear, filesDir); 38 | 39 | // Report finish 40 | workerPort.postMessage("done"); 41 | } 42 | }; 43 | 44 | /** 45 | * Defines the event handler to be called when the worker receives a message that cannot be deserialized. 46 | * The event handler is executed in the worker thread. 47 | * 48 | * @param event message data 49 | */ 50 | workerPort.onmessageerror = (event: MessageEvents) => { 51 | }; 52 | 53 | /** 54 | * Defines the event handler to be called when an exception occurs during worker execution. 55 | * The event handler is executed in the worker thread. 56 | * 57 | * @param event error message 58 | */ 59 | workerPort.onerror = (event: ErrorEvent) => { 60 | }; 61 | 62 | /** 63 | * Saves index set to sandbox 64 | * @param map 65 | * @param context_filesDir 66 | * */ 67 | function save_index_ext(file_name: string, map: collections.Map>, clear: boolean, context_filesDir: string) { 68 | let filesDir: string = context_filesDir; 69 | let result: string[] = []; 70 | let index = 0; 71 | 72 | if (clear) { 73 | // DONE: Update this worker to adapt to the monthly division style of maps. 74 | // Clear all previous index 75 | try { 76 | fileIo.rmdirSync(context_filesDir + "/history-index"); 77 | fileIo.mkdirSync(context_filesDir + "/history-index", true); 78 | } catch (e) { 79 | 80 | } 81 | } 82 | 83 | // Timer 84 | let s = Date.now(); 85 | console.log(bunch_of_history_index.log_head_worker() + '[Save] Start to save map "' + file_name + '"! Map size: ' + map.size.toString() + ".") 86 | 87 | map.forEach((value, key) => { 88 | result.push(key) 89 | result.push(value.join("_")); 90 | // Report status 91 | if (index % 1000 == 0) { 92 | const progress = index / map.size * 100; 93 | workerPort.postMessage((Date.now() - s).toString() + " ms, " + (progress.toFixed(2) + "%")); 94 | } 95 | index += 1; 96 | }) 97 | 98 | console.log(bunch_of_history_index.log_head_worker() + '[Save] Finished forEach traversal! Time used: ' + (Date.now() - s).toString() + " ms.") 99 | 100 | // console.log("qwq " + result.toString()) 101 | sandbox_save('history-index/' + file_name, result.join('\n'), filesDir); 102 | workerPort.postMessage((Date.now() - s).toString() + " ms, 100%"); 103 | 104 | // Timer end 105 | console.log(bunch_of_history_index.log_head_worker() + "[Save] Finished saving to disk in " + (Date.now() - s).toString() + " ms.") 106 | } -------------------------------------------------------------------------------- /home/src/main/ets/hosts/bunch_of_search_engines.ets: -------------------------------------------------------------------------------- 1 | import { default_search_engine } from './bunch_of_defaults'; 2 | import { bunch_of_settings } from './bunch_of_settings'; 3 | 4 | export class bunch_of_search_engines { 5 | static list_of_search_engines: search_engine[] = []; 6 | 7 | /** 8 | * Update last accessed time. This would trigger some refresh on UI layer. 9 | * */ 10 | static update_last_accessed() { 11 | AppStorage.setOrCreate('bunch_of_search_engines_update', Date.now()); 12 | } 13 | 14 | /** 15 | * Sets the global SE. 16 | * @param idx The index of SE. 17 | * */ 18 | static set_global_SE(idx: number) { 19 | if (idx == -99) { 20 | // Setting index to -99 then the real value 21 | // Can force refresh @Watch 22 | // And the index -99 has no meaning 23 | return; 24 | } 25 | AppStorage.setOrCreate('search_engine_selected', idx); 26 | if (idx <= -1) { 27 | // If set back to default 28 | AppStorage.setOrCreate('search_engine', default_search_engine()); 29 | } else { 30 | AppStorage.setOrCreate('search_engine', bunch_of_search_engines.list_of_search_engines[idx].url); 31 | } 32 | bunch_of_settings.set('custom_search_engines_selected_index', idx); 33 | } 34 | 35 | /** 36 | * Add a new search engine to the current search engine list. 37 | * @param se A search_engine object, the search engine to be added. 38 | * */ 39 | static add_search_engine(se: search_engine) { 40 | bunch_of_search_engines.list_of_search_engines.push(se); 41 | bunch_of_search_engines.update_last_accessed(); 42 | } 43 | 44 | /** 45 | * Remove a search engine from the current search engine list. 46 | * @param index A number, the index of the search engine to be removed in the list. 47 | * */ 48 | static del_search_engine(index: number) { 49 | bunch_of_search_engines.list_of_search_engines.splice(index, 1); 50 | bunch_of_search_engines.update_last_accessed(); 51 | } 52 | 53 | /** 54 | * Export search engines in a specific plain text format. 55 | * @returns '\n' connected string of search engines in the format of: 56 | * @example 'Bing\nhttps://www.cn.bing.com/search?q=%s\nGoogle\nhttps://www.google.com/search?q=%s' 57 | * */ 58 | static export_string() { 59 | let export_list: string[] = [] 60 | for (let index = 0; index < bunch_of_search_engines.list_of_search_engines.length; index++) { 61 | let ua: search_engine = bunch_of_search_engines.list_of_search_engines[index]; 62 | export_list.push(ua.label); 63 | export_list.push(ua.url); 64 | } 65 | return export_list.join("\n"); 66 | } 67 | 68 | /** 69 | * Import search engines in a specific plain text format. 70 | * In default overwrites whatever was in this list_of_search_engines. 71 | * @param imp The string in the correct format: 72 | * @example 'Bing\nhttps://www.cn.bing.com/search?q=%s\nGoogle\nhttps://www.google.com/search?q=%s' 73 | * */ 74 | static import_string(imp: string) { 75 | // Clear 76 | bunch_of_search_engines.list_of_search_engines = []; 77 | if (imp == "") { 78 | return; 79 | } 80 | // Import 81 | let import_list: string[] = imp.split("\n"); 82 | for (let index = 0; index < import_list.length; index += 2) { 83 | if (import_list[index] != "") { 84 | bunch_of_search_engines.add_search_engine(new search_engine(import_list[index], import_list[index+1])); 85 | } 86 | } 87 | } 88 | } 89 | 90 | export class search_engine { 91 | label: string; 92 | url: string; 93 | 94 | /** 95 | * A search_engine object, which consists of a label and the search engine link. 96 | * @param label The name of the search engine. 97 | * @param link The link of the search engine, '%s' in which will be replaced with real search keywords. 98 | */ 99 | constructor(label: string, content: string) { 100 | this.label = label; 101 | this.url = content; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/quicks/woofQuickSE.ets: -------------------------------------------------------------------------------- 1 | import { animation_default, click_effect_default, default_search_engine } from '../../hosts/bunch_of_defaults'; 2 | import { bunch_of_search_engines, search_engine } from '../../hosts/bunch_of_search_engines'; 3 | import lazy woofControlFrame from '../woofControlFrame'; 4 | import meowSEManager from '../../blocks/panels/meowSEManager'; 5 | import linysText from '../../components/texts/linysText'; 6 | 7 | @CustomDialog 8 | struct woofQuickSE { 9 | controller: CustomDialogController; 10 | @Prop keyword: string = '' 11 | @Prop default_new_se: search_engine = new search_engine(new Date().toLocaleString(), default_search_engine()); 12 | // Colors 13 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 14 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 15 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 16 | // Settings - Accessibility 17 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 18 | // Animation UI Statuses 19 | @State adding_SE: boolean = false; 20 | @State height_of_add_panel: number = 0; 21 | button_height_default: number = 42; 22 | // Gateways 23 | @LocalStorageLink('universal_load_url_gateway') load_url_gateway: string = ""; 24 | // Count 25 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 26 | 27 | aboutToAppear(): void { 28 | this.opened_dialog_controllers.push(this.controller); 29 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 30 | } 31 | 32 | aboutToDisappear(): void { 33 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 34 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 35 | } 36 | 37 | build() { 38 | woofControlFrame({ 39 | title: $r('app.string.AddSE_search_again_on'), 40 | controller: this.controller 41 | }) { 42 | Scroll() { 43 | Column({ space: 10 }) { 44 | linysText({ 45 | text: ' 󰀩 ' + this.keyword + ' ' 46 | }) 47 | .padding(10) 48 | .borderRadius(13.5) 49 | .clickEffect(click_effect_default()) 50 | .backgroundColor(this.color_current_secondary) 51 | 52 | meowSEManager({ 53 | default_new_se: this.default_new_se, 54 | on_default_execution: () => { 55 | this.search_with_se(this.keyword); 56 | }, 57 | on_normal_execution: (idx, _se) => { 58 | this.search_with_se(this.keyword, idx); 59 | } 60 | }) 61 | } 62 | .alignItems(HorizontalAlign.Start) 63 | } 64 | .borderRadius(13.5) 65 | .layoutWeight(1) 66 | .scrollBar(BarState.Off) 67 | .edgeEffect(EdgeEffect.Spring) 68 | .constraintSize({ 69 | maxHeight: 45 + 48.5 * bunch_of_search_engines.list_of_search_engines.length + 100 + (this.adding_SE ? 20 + this.height_of_add_panel : 0) 70 | }) 71 | .animation(animation_default()) 72 | .onAppear(() => { 73 | console.log('[woofQuickSE] About to search: "' + this.keyword + '" on another engine.') 74 | }) 75 | } 76 | } 77 | 78 | search_with_se(key: string, clicked_index?: number) { 79 | // Choose to search on another engine 80 | if (clicked_index !== undefined) { 81 | this.load_url_gateway = bunch_of_search_engines.list_of_search_engines[clicked_index].url.replaceAll('%s', key); 82 | } else { 83 | this.load_url_gateway = default_search_engine().replaceAll('%s', key); 84 | } 85 | console.log('[woofQuickSE] Search: "' + this.keyword + '" => ' + this.load_url_gateway); 86 | if (this.controller) { 87 | this.controller.close(); 88 | } 89 | } 90 | } 91 | 92 | export default woofQuickSE; -------------------------------------------------------------------------------- /home/src/main/ets/blocks/panels/meowHomepageManager.ets: -------------------------------------------------------------------------------- 1 | import lazy { 2 | arrayBuffer_2_pixelMap_sync, 3 | image_document_pick_to_ArrayBuffer, 4 | image_gallery_pick_to_ArrayBuffer, 5 | sandbox_save, 6 | sandbox_unlink 7 | } from '../../utils/storage_tools'; 8 | import linysCapsuleButtonWithText from '../../components/buttons/linysCapsuleButtonWithText'; 9 | import linysTimeoutButtonWithText from '../../components/buttons/linysTimeoutButtonWithText'; 10 | 11 | @Component 12 | struct meowHomepageManager { 13 | // Gateways 14 | @LocalStorageLink('universal_fail_prompt_desc_gateway') uni_fail_prompt_gateway: ResourceStr = ""; 15 | // Environments 16 | @StorageLink('homepage_background') homepage_background: PixelMap | undefined = undefined; 17 | // Colors 18 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 19 | @StorageLink('color_current_secondary') color_current_secondary: ResourceColor = $r('app.color.block_color'); 20 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 21 | // Settings / Accessibility 22 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 23 | 24 | build() { 25 | Column({ space: 10 }) { 26 | linysCapsuleButtonWithText({ 27 | desc_text: $r('app.string.Homepage_background_select_storage'), 28 | button_text: " 󰃟 ", 29 | onExecution: () => { 30 | this.select_set_background_image_storage(); 31 | } 32 | }) // Homepage background select from files 33 | 34 | linysCapsuleButtonWithText({ 35 | desc_text: $r('app.string.Homepage_background_select_gallery'), 36 | button_text: " 󰓥 ", 37 | onExecution: () => { 38 | this.select_set_background_image_gallery(); 39 | } 40 | }) // Homepage background select from files 41 | 42 | linysTimeoutButtonWithText({ 43 | desc_text: $r('app.string.Homepage_background_delete'), 44 | button_text: " 󰀁 ", 45 | onExecution: () => { 46 | this.delete_homepage_background(); 47 | } 48 | }) // Homepage background delete 49 | } 50 | .alignItems(this.preferred_hand_left_or_right == 'right' ? HorizontalAlign.End : HorizontalAlign.Start) 51 | } 52 | 53 | /** 54 | * Selects an image from storage. 55 | * */ 56 | select_set_background_image_storage() { 57 | image_document_pick_to_ArrayBuffer().then(result => { 58 | if (result !== undefined) { 59 | let buf = result as ArrayBuffer; 60 | // Set image for homepage 61 | this.homepage_background = arrayBuffer_2_pixelMap_sync(buf); 62 | // Save this to sandbox 63 | sandbox_save('homepage_background_arrayBuffer', buf); 64 | } else { 65 | console.log('[meowAppSettings] Select homepage background failed. Received undefined?') 66 | this.uni_fail_prompt_gateway = $r('app.string.Fail_select_bad_image'); 67 | } 68 | }); 69 | } 70 | 71 | /** 72 | * Selects an image from gallery. 73 | * */ 74 | select_set_background_image_gallery() { 75 | image_gallery_pick_to_ArrayBuffer().then(result => { 76 | if (result !== undefined) { 77 | let buf = result as ArrayBuffer; 78 | // Set image for homepage 79 | this.homepage_background = arrayBuffer_2_pixelMap_sync(buf); 80 | // Save this to sandbox 81 | sandbox_save('homepage_background_arrayBuffer', buf); 82 | } else { 83 | console.log('[meowAppSettings] Select homepage background failed. Received undefined?') 84 | this.uni_fail_prompt_gateway = $r('app.string.Fail_select_bad_image'); 85 | } 86 | }); 87 | } 88 | 89 | delete_homepage_background() { 90 | // Delete homepage background image 91 | this.homepage_background = undefined; 92 | sandbox_unlink('homepage_background_arrayBuffer'); 93 | console.log("[meowAppSettings] Deleted homepage background!") 94 | } 95 | } 96 | 97 | export default meowHomepageManager; -------------------------------------------------------------------------------- /home/src/main/resources/rawfile/cathrome/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * StandaloneI18n Class 3 | * * This class provides a basic implementation of the Chrome i18n API 4 | * (chrome.i18n) for use in environments where the native API is unavailable, 5 | * such as non-extension web pages or testing frameworks. 6 | * * NOTE: This is a structural and functional mimic. It does not handle 7 | * real-world locale file loading (_locales/xx/messages.json) or advanced 8 | * Chrome features like Bidi-UI. 9 | */ 10 | class cathrome_i18n { 11 | constructor() { 12 | // Log initialization success 13 | console.log(`[i18n] StandaloneI18n initialized!`); 14 | } 15 | 16 | /** 17 | * Gets the localized string for the specified message name. 18 | * Substitutions using $1, $2, etc., are supported, mimicking Chrome's behavior. 19 | * * @param {string} messageName - The name of the message to retrieve. 20 | * @param {string|string[]|null} substitutions - A single string or an array of substitution strings. 21 | * @returns {string} The localized message, or an error message if not found. 22 | */ 23 | getMessage(messageName, substitutions = [], options = {}) { 24 | let message = CatsBridge.chrome_i18n_getMessage(messageName); 25 | 26 | // 1. Handle substitutions 27 | const subsArray = Array.isArray(substitutions) ? substitutions : (substitutions ? [substitutions] : []); 28 | 29 | // Iterate through substitutions (up to $99) 30 | for (let i = 0; i < subsArray.length; i++) { 31 | // Replace $1, $2, etc. with the substitution string 32 | const placeholder = new RegExp('\\$' + (i + 1), 'g'); 33 | message = message.replace(placeholder, subsArray[i]); 34 | } 35 | 36 | // 2. Handle escape sequences: replace '$$' with '$' 37 | message = message.replace(/\$\$/g, '$'); 38 | 39 | const escapeLt = options.escapeLt; 40 | if (escapeLt) { 41 | // ? 42 | } 43 | 44 | return message; 45 | } 46 | 47 | /** 48 | * Gets the browser's UI language. In this standalone version, it returns the 49 | * locale provided during initialization. 50 | * * @returns {string} The application's UI language. 51 | */ 52 | getUILanguage() { 53 | return CatsBridge.chrome_i18n_getUILanguage(); 54 | } 55 | 56 | /** 57 | * Gets the language list that the browser supports. 58 | * This is an asynchronous method in the Chrome API, so we mimic the structure. 59 | * * NOTE: In a real implementation, you'd fetch this from the environment 60 | * (e.g., navigator.languages) or return it directly if synchronous is acceptable. 61 | * * @param {function(string[]): void} callback - A callback function that accepts the language array. 62 | */ 63 | getAcceptLanguages(callback) { 64 | // Mocking the asynchronous call with a hardcoded list and a setTimeout. 65 | const mockLanguages = CatsBridge.chrome_i18n_getAcceptLanguages(); 66 | 67 | // Use setTimeout to mimic the asynchronous nature of a Chrome API call 68 | setTimeout(() => { 69 | if (typeof callback === 'function') { 70 | callback(mockLanguages); 71 | } 72 | }, 0); 73 | } 74 | 75 | /** 76 | * Detects the language of the provided text. 77 | * This method mimics the asynchronous signature and return structure of the Chrome API. 78 | * * @param {string} text - The text to analyze for language. 79 | * @param {function(Object): void} callback - A callback function that accepts the detection result object. 80 | */ 81 | detectLanguage(text, callback) { 82 | // Simple, mock language detection logic based on keywords 83 | const normalizedText = text.toLowerCase(); 84 | 85 | // Construct the result object as per Chrome API specification 86 | const result = { 87 | isReliable: isReliable, 88 | languages: [{ 89 | language: 'und', 90 | percentage: 49 // Mock percentage 91 | }] 92 | }; 93 | 94 | // Use setTimeout to mimic the asynchronous nature of a Chrome API call 95 | setTimeout(() => { 96 | if (typeof callback === 'function') { 97 | callback(result); 98 | } 99 | }, 0); 100 | } 101 | } -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/web/woofHttpAuthPrompt.ets: -------------------------------------------------------------------------------- 1 | import linysConfirmDenyButtons from '../../components/buttons/linysConfirmDenyButtons'; 2 | import linysText from '../../components/texts/linysText'; 3 | import linysTextTitle from '../../components/texts/linysTextTitle'; 4 | import { capsule_bar_height } from '../../hosts/bunch_of_defaults'; 5 | 6 | @CustomDialog 7 | struct woofHttpAuthPrompt { 8 | @Link e: OnHttpAuthRequestEvent | undefined; 9 | username: string = 'Username Meow'; 10 | password: string = 'Password Meow'; 11 | controller: CustomDialogController; 12 | @StorageLink('woofHttpAuthPrompt_result') woofHttpAuthPrompt_result: boolean = false; 13 | // Colors 14 | @StorageLink('effects') effects: boolean = false; 15 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 16 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 17 | // UI display 18 | @LocalStorageLink('screen_height') screen_height: number = 0; 19 | // Count 20 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 21 | 22 | aboutToAppear(): void { 23 | this.opened_dialog_controllers.push(this.controller); 24 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 25 | } 26 | 27 | aboutToDisappear(): void { 28 | this.cancel(); 29 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 30 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 31 | } 32 | 33 | build() { 34 | Column({ space: 15 }) { 35 | linysTextTitle({ text: this.e?.host + ' [' + this.e?.realm + ']', max_lines: 2 }) // Page url 36 | .width("100%") 37 | 38 | linysText({ text: $r('app.string.http_auth_desc'), max_lines: 2 }) // Prompt message 39 | .width("100%") 40 | 41 | TextInput({ text: '', placeholder: $r('app.string.username') }) // Username 42 | .onChange((value) => { 43 | this.username = value; 44 | }) 45 | .fontWeight(FontWeight.Regular) 46 | .fontColor(this.color_current_font) 47 | .caretColor(this.color_current_font) 48 | .selectedBackgroundColor(this.color_current_font) 49 | .id('woofHttpAuthPrompt_username') 50 | .onSubmit(() => { 51 | focusControl.requestFocus('woofHttpAuthPrompt_password'); 52 | }) 53 | .height(capsule_bar_height()) 54 | .onAppear(() => { 55 | focusControl.requestFocus('woofHttpAuthPrompt_username'); 56 | }) 57 | 58 | TextInput({ text: '', placeholder: $r('app.string.password') }) // Password 59 | .onChange((value) => { 60 | this.password = value; 61 | }) 62 | .fontWeight(FontWeight.Regular) 63 | .fontColor(this.color_current_font) 64 | .caretColor(this.color_current_font) 65 | .selectedBackgroundColor(this.color_current_font) 66 | .id('woofHttpAuthPrompt_password') 67 | .onSubmit(() => { 68 | // Submit 69 | this.submit(); 70 | if (this.controller) { 71 | this.controller.close(); 72 | } 73 | }) 74 | .height(capsule_bar_height()) 75 | 76 | linysConfirmDenyButtons({ 77 | onConfirm: () => { 78 | this.submit(); 79 | if (this.controller) { 80 | this.controller.close(); 81 | } 82 | }, 83 | onDeny: () => { 84 | this.cancel(); 85 | if (this.controller) { 86 | this.controller.close(); 87 | } 88 | }, 89 | deny_double_confirm: true, 90 | confirm_double_confirm: true, 91 | }) // Buttons 92 | } 93 | .padding(20) 94 | .alignItems(HorizontalAlign.Start) 95 | .justifyContent(FlexAlign.Center) 96 | .width("100%") 97 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 98 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 99 | } 100 | 101 | submit() { 102 | this.woofHttpAuthPrompt_result = true; 103 | this.e?.handler.confirm(this.username, this.password); 104 | } 105 | 106 | cancel() { 107 | this.woofHttpAuthPrompt_result = false; 108 | this.e?.handler.cancel(); 109 | } 110 | } 111 | 112 | export default woofHttpAuthPrompt; -------------------------------------------------------------------------------- /home/src/main/ets/workers/History_index_loader.ets: -------------------------------------------------------------------------------- 1 | import { collections, ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; 2 | import { bunch_of_history_index } from '../hosts/bunch_of_history_index'; 3 | import { fileIo } from '@kit.CoreFileKit'; 4 | import { bunch_of_history_index_lite as month_host } from '../hosts/bunch_of_history_index_lite'; 5 | 6 | const workerPort: ThreadWorkerGlobalScope = worker.workerPort; 7 | let threshold: number = 0; 8 | 9 | /** 10 | * Defines the event handler to be called when the worker thread receives a message sent by the host thread. 11 | * The event handler is executed in the worker thread. 12 | * 13 | * @param event message data 14 | */ 15 | workerPort.onmessage = (event: MessageEvents) => { 16 | // receive getContext().filesDir from main thread 17 | if (typeof event.data == "number") { 18 | // threshold of load index files amount 19 | threshold = event.data as number; 20 | } 21 | if (typeof event.data == "string") { 22 | // Receive the filesDir 23 | let filesDir = event.data as string; 24 | // Execution 25 | sandbox_load_index_sync(filesDir, threshold); 26 | // Send back information 27 | let back = new collections.Array>>(); 28 | back.push(bunch_of_history_index.index_map); 29 | back.push(month_host.index_map); 30 | workerPort.postMessageWithSharedSendable(back); 31 | } 32 | }; 33 | 34 | /** 35 | * Defines the event handler to be called when the worker receives a message that cannot be deserialized. 36 | * The event handler is executed in the worker thread. 37 | * 38 | * @param event message data 39 | */ 40 | workerPort.onmessageerror = (event: MessageEvents) => { 41 | }; 42 | 43 | /** 44 | * Defines the event handler to be called when an exception occurs during worker execution. 45 | * The event handler is executed in the worker thread. 46 | * 47 | * @param event error message 48 | */ 49 | workerPort.onerror = (event: ErrorEvent) => { 50 | }; 51 | 52 | /** 53 | * Loads built index from disk. 54 | * */ 55 | function sandbox_load_index_sync(context_filesDir: string, load_threshold: number = 0) { 56 | let s = Date.now(); 57 | console.log(bunch_of_history_index.log_head_worker() + '[Load] Start loading from sandbox! Start: ' + new Date(s).toString() + '.') 58 | 59 | let all_index_files = fileIo.listFileSync(context_filesDir + '/history-index', { recursion: false }).sort(); 60 | 61 | console.log(bunch_of_history_index.log_head_worker() + '[Load][listFileSync] all files: ' + all_index_files.join(", ")); 62 | 63 | if (all_index_files.length == 0) { 64 | console.log(bunch_of_history_index.log_head_worker() + ' No index found. ') 65 | // console.log(bunch_of_history_index.log_head_worker() + ' Rebuilding... ') 66 | // history_index_full_rebuild_worker(); 67 | return; 68 | } 69 | 70 | // Limit history index months 71 | // let load_threshold = AppStorage.get('history_index_load_quantity') as number; 72 | if (load_threshold != 0 && all_index_files.length > load_threshold) { 73 | all_index_files = all_index_files.slice(all_index_files.length - load_threshold, all_index_files.length); 74 | } 75 | 76 | workerPort.postMessage((Date.now() - s).toString() + " ms, 0.01%"); 77 | 78 | // clear 79 | bunch_of_history_index.clear(); 80 | 81 | // Read every file under history-index 82 | for (let index = 0; index < all_index_files.length; index++) { 83 | const name = all_index_files[index]; 84 | 85 | bunch_of_history_index.index_from_index_file('history-index/' + name, context_filesDir); 86 | 87 | workerPort.postMessage((Date.now() - s).toString() + " ms, " + (index / all_index_files.length * 90).toFixed(2) + "%"); 88 | // console.log(bunch_of_history_index.log_head_worker() + '[Load] Finish loading: ' + name + '.'); 89 | } 90 | 91 | // Load this month to a separate host 92 | month_host.clear(); 93 | const current_year: number = new Date().getUTCFullYear(); 94 | const current_month: number = new Date().getUTCMonth() + 1; 95 | month_host.index_from_index_month_file(current_year, current_month, context_filesDir); 96 | 97 | // End report 98 | console.log(bunch_of_history_index.log_head_worker() + '[Load] Finish loading from sandbox! Time used: ' + (Date.now() - s).toString() + ' ms. ' + 99 | 'Map size: ' + bunch_of_history_index.index_map.size.toString() + '.'); 100 | 101 | workerPort.postMessage((Date.now() - s).toString() + " ms, 100%"); 102 | } -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/web/woofPrompt.ets: -------------------------------------------------------------------------------- 1 | import linysConfirmDenyButtons from '../../components/buttons/linysConfirmDenyButtons'; 2 | import linysText from '../../components/texts/linysText'; 3 | import linysTextArea from '../../components/texts/linysTextArea'; 4 | import linysTextTitle from '../../components/texts/linysTextTitle'; 5 | import { capsule_bar_height } from '../../hosts/bunch_of_defaults'; 6 | 7 | @CustomDialog 8 | struct woofPrompt { 9 | @State title: ResourceStr = 'Title Meow'; 10 | @State message: ResourceStr = 'Message Meow'; 11 | @State value: string = 'Value Meow'; 12 | @Link promptResult: string; 13 | single_line: boolean = false; 14 | controller: CustomDialogController; 15 | // Colors 16 | @StorageLink('effects') effects: boolean = false; 17 | @StorageLink('color_current_primary') color_current_primary: ResourceColor = $r('app.color.start_window_background'); 18 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 19 | // UI display 20 | @LocalStorageLink('screen_height') screen_height: number = 0; 21 | // Actions 22 | onConfirm?: () => void; 23 | onDeny?: () => void; 24 | // Count 25 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 26 | // Focus 27 | my_id: string = 'woofHistory_search_input_' + Date.now().toString(); 28 | 29 | aboutToAppear(): void { 30 | this.opened_dialog_controllers.push(this.controller); 31 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 32 | } 33 | 34 | aboutToDisappear(): void { 35 | if (this.onDeny) { 36 | this.onDeny(); 37 | } 38 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 39 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 40 | } 41 | 42 | build() { 43 | Column({ space: 15 }) { 44 | linysTextTitle({ text: this.title, max_lines: 2 }) // Page url 45 | .width("100%") 46 | 47 | linysText({ text: this.message, max_lines: 999 }) // Prompt message 48 | .width("100%") 49 | 50 | if (this.single_line) { 51 | TextInput({ text: this.value }) 52 | .onChange((value) => { 53 | this.promptResult = value; 54 | }) 55 | .fontWeight(FontWeight.Regular) 56 | .fontColor(this.color_current_font) 57 | .caretColor(this.color_current_font) 58 | .selectedBackgroundColor(this.color_current_font) 59 | .onSubmit(() => { 60 | if (this.controller) { 61 | if (this.onConfirm) { 62 | this.onConfirm(); 63 | } 64 | this.controller.close(); 65 | } 66 | }) 67 | .height(capsule_bar_height()) 68 | .id(this.my_id) 69 | .onAppear(() => { 70 | // Auto focus on history 71 | if (AppStorage.get('free_window_mode') as boolean) { 72 | this.focus_input(); 73 | } 74 | }) 75 | } else { 76 | linysTextArea({ init_text: this.value, text: this.promptResult }) // Edit 77 | .constraintSize({ maxHeight: Math.max(0, this.screen_height - 160) }) 78 | .id(this.my_id) 79 | .onAppear(() => { 80 | // Auto focus on history 81 | if (AppStorage.get('free_window_mode') as boolean) { 82 | this.focus_input(); 83 | } 84 | }) 85 | } 86 | 87 | linysConfirmDenyButtons({ 88 | onConfirm: () => { 89 | if (this.controller) { 90 | if (this.onConfirm) { 91 | this.onConfirm(); 92 | } 93 | this.controller.close(); 94 | } 95 | }, 96 | onDeny: () => { 97 | if (this.controller) { 98 | if (this.onDeny) { 99 | this.onDeny(); 100 | } 101 | this.controller.close(); 102 | } 103 | }, 104 | deny_double_confirm: true, 105 | confirm_double_confirm: true, 106 | }) // Buttons 107 | } 108 | .padding(20) 109 | .alignItems(HorizontalAlign.Start) 110 | .justifyContent(FlexAlign.Center) 111 | .width("100%") 112 | .backgroundColor(this.effects ? undefined : this.color_current_primary) 113 | .useEffect(this.effects, EffectType.WINDOW_EFFECT) 114 | } 115 | 116 | /** 117 | * Focus on the search input. 118 | * */ 119 | focus_input() { 120 | focusControl.requestFocus(this.my_id); 121 | } 122 | } 123 | 124 | export default woofPrompt; -------------------------------------------------------------------------------- /home/src/main/ets/hosts/bunch_of_defaults.ets: -------------------------------------------------------------------------------- 1 | import lazy Curves from '@ohos.curves'; 2 | import lazy { resource_to_string } from '../utils/resource_tools'; 3 | 4 | /** 5 | * Default fontSize_Extra for texts. 6 | * 7 | * Usually used on titles. 8 | * @returns 26 9 | * */ 10 | export function fontSize_Extra() { 11 | return 24; 12 | } 13 | 14 | /** 15 | * Default fontSize_Large for texts. 16 | * 17 | * Usually used on sub titles. 18 | * @returns 20 19 | * */ 20 | export function fontSize_Large() { 21 | return 20; 22 | } 23 | 24 | /** 25 | * Default fontSize_Normal for texts. 26 | * 27 | * Usually used on body parts. 28 | * @returns 16 29 | * */ 30 | export function fontSize_Normal() { 31 | return 16; 32 | } 33 | 34 | /** 35 | * Default fontSize_Icon_Button for HarmonyOS Symbols. 36 | * 37 | * Usually used on buttons, like linysSymbol. 38 | * @returns 25 39 | * */ 40 | export function fontSize_Icon_Button() { 41 | return 25; 42 | } 43 | 44 | /** 45 | * Default capsule bar height for capsule-shape components, like a capsule button. 46 | * @returns 35 47 | * */ 48 | export function capsule_bar_height() { 49 | return 35; 50 | } 51 | 52 | /** 53 | * Default minimum width for cards in panels, like those in settings and downloads panel. 54 | * @returns 480 55 | * */ 56 | export function minimum_card_width() { 57 | return 480; 58 | } 59 | 60 | /** 61 | * Default animation params. 62 | * @returns An AnimateParam, reading the arguments from app settings. 63 | * */ 64 | export function animation_default() { 65 | let animation_damping_coefficient = AppStorage.get('animation_damping_coefficient') as number; 66 | let animation_response = AppStorage.get('animation_response') as number; 67 | let spring = Curves.springMotion(animation_response / 100, (100 - animation_damping_coefficient) / 100); 68 | let ap: AnimateParam = { curve: spring }; 69 | return ap; 70 | } 71 | 72 | /** 73 | * Standard popup notice duration for intervals of 1 millisecond, 2 seconds. 74 | * @returns 2000 75 | * */ 76 | export function animation_popup_duration() { 77 | return 1000; 78 | } 79 | 80 | /** 81 | * Default Click effect. 82 | * @returns ClickEffect = { level: ClickEffectLevel.LIGHT } 83 | * */ 84 | export function click_effect_default() { 85 | let ce: ClickEffect = { level: ClickEffectLevel.LIGHT }; 86 | return ce; 87 | } 88 | 89 | /** 90 | * Default link of blank page. 91 | * @returns 'meow://home' 92 | * */ 93 | export function url_default_blank() { 94 | return 'meow://home'; 95 | } 96 | 97 | /** 98 | * Preset search engines in a specified string format. 99 | * @returns Baidu and Google. 100 | * @example 'Bing\nhttps://www.cn.bing.com/search?q=%s' 101 | * */ 102 | export function preset_search_engines() { 103 | let result = [ 104 | "Baidu\nhttps://www.baidu.com/s?wd=%s", 105 | "Google\nhttps://www.google.com/search?q=%s", 106 | ]; 107 | return result.join("\n"); 108 | } 109 | 110 | /** 111 | * The single default search engine. 112 | * @returns 'https://bing.com/search?q=%s' 113 | * */ 114 | export function default_search_engine() { 115 | return 'https://bing.com/search?q=%s'; 116 | } 117 | 118 | /** 119 | * Default user agents in a specified string format. 120 | * @returns Firefox, Chrome and Edge. 121 | * @example 'Firefox Windows 132\nMozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0' 122 | * */ 123 | export function default_user_agents() { 124 | let result = [ 125 | "Huawei Browser 132\nMozilla/5.0 (PC; OpenHarmony 6.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 ArkWeb/6.0.0.107 HuaweiBrowser/233.1.9.311", 126 | "Firefox Windows 132\nMozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0", 127 | "Edge Windows 132\nMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/130.0.0.0", 128 | "Chrome Windows 132\nMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36", 129 | "Chrome Android 132\nMozilla/5.0 (Linux; Android 12; ALN-AL80 Build/5.0.0.107) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Mobile Safari/537.36", 130 | ]; 131 | return result.join("\n"); 132 | } 133 | 134 | /** 135 | * The default name of a window. 136 | * @returns 'Foxy Meowy Dodgy! Welcome to a new window!' 137 | * */ 138 | export function default_new_window_name(): string { 139 | return resource_to_string($r('app.string.Settings_appearance_windows_default_name')); 140 | } 141 | 142 | /** 143 | * Obtains the default window_decor_height. 144 | * @returns A number, in vp. 145 | * */ 146 | export function default_window_decor_height(): number { 147 | return (AppStorage.get('showing_my_window_alias') as boolean) ? 45 : 37; 148 | } -------------------------------------------------------------------------------- /home/src/main/ets/objects/ExtWebNode.ets: -------------------------------------------------------------------------------- 1 | import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI'; 2 | import { on_new_window } from '../processes/web_actions'; 3 | import lazy { hilog } from '@kit.PerformanceAnalysisKit'; 4 | import { extension } from '../hosts/bunch_of_extensions'; 5 | import { CatsBridgeMethods } from './CatstensionBridgeObj'; 6 | import { text_from_rawfile } from '../utils/storage_tools'; 7 | 8 | @Builder 9 | function WebBuilder(ext: extension) { 10 | Web({ controller: ext.controller, src: $rawfile('home.html') }) 11 | .mediaPlayGestureAccess(false) 12 | .databaseAccess(true) 13 | .fileAccess(true) 14 | .domStorageAccess(true) 15 | .multiWindowAccess(true) 16 | .mixedMode(MixedMode.All) 17 | .allowWindowOpenMethod(true) 18 | .mediaOptions({ audioExclusive: false }) 19 | .darkMode(WebDarkMode.Auto) // Dark mode! 20 | .javaScriptAccess(true) 21 | .imageAccess(true) 22 | .onlineImageAccess(true) 23 | // .layoutMode(WebLayoutMode.FIT_CONTENT) 24 | .onWindowNew((event) => { 25 | if (ext.storage) { 26 | on_new_window(ext.storage, event); 27 | } else { 28 | event.handler.setWebController(null); 29 | } 30 | }) 31 | .onPageEnd((e) => { 32 | console.log(`[Extension][${ext.name}][${ext.id}] Web Loaded! url=${e.url}`); 33 | }) 34 | .javaScriptProxy({ 35 | object: ext.bridge, 36 | name: 'CatsBridge', 37 | methodList: CatsBridgeMethods(), 38 | controller: ext.controller 39 | }) 40 | .runJavaScriptOnDocumentStart([ 41 | { script: text_from_rawfile('cathrome/eventListener.js'), scriptRules: ["*"] }, 42 | { script: text_from_rawfile('cathrome/liny.js'), scriptRules: ["*"] }, 43 | { script: text_from_rawfile('cathrome/alarms.js'), scriptRules: ["*"] }, 44 | { script: text_from_rawfile('cathrome/action.js'), scriptRules: ["*"] }, 45 | { script: text_from_rawfile('cathrome/bookmarks.js'), scriptRules: ["*"] }, 46 | { script: text_from_rawfile('cathrome/browsingData.js'), scriptRules: ["*"] }, 47 | { script: text_from_rawfile('cathrome/commands.js'), scriptRules: ["*"] }, 48 | { script: text_from_rawfile('cathrome/contentSettings.js'), scriptRules: ["*"] }, 49 | { script: text_from_rawfile('cathrome/contextMenus.js'), scriptRules: ["*"] }, 50 | { script: text_from_rawfile('cathrome/declarativeNetRequest.js'), scriptRules: ["*"] }, 51 | { script: text_from_rawfile('cathrome/i18n.js'), scriptRules: ["*"] }, 52 | { script: text_from_rawfile('cathrome/cathrome.js'), scriptRules: ["*"] }, 53 | ]) 54 | .onConsole((e) => { 55 | let info = `[Extension][${ext.name}][${ext.id}]: ` + e.message.getMessage(); 56 | if (e.message.getMessageLevel() == MessageLevel.Error) { 57 | console.error(info); 58 | } else if (e.message.getMessageLevel() == MessageLevel.Warn) { 59 | console.warn(info); 60 | } else { 61 | console.log(info); 62 | } 63 | return true; 64 | }) 65 | } 66 | 67 | export class ExtNodeController extends NodeController { 68 | private builderNode: BuilderNode<[extension]> | null | undefined = null; 69 | private rootNode: FrameNode | null = null; 70 | 71 | constructor(builderNode: BuilderNode<[extension]> | undefined) { 72 | super(); 73 | this.builderNode = builderNode; 74 | } 75 | 76 | makeNode(uiContext: UIContext): FrameNode | null { 77 | return this.rootNode; 78 | } 79 | 80 | attachWeb(): void { 81 | if (this.builderNode) { 82 | let frameNode: FrameNode | null = this.builderNode.getFrameNode(); 83 | if (frameNode?.getParent() != null) { 84 | hilog.error(0x0000, 'testTag', '%{public}s', 'The frameNode is already attached'); 85 | return; 86 | } 87 | this.rootNode = this.builderNode.getFrameNode(); 88 | } 89 | } 90 | 91 | detachWeb(): void { 92 | this.rootNode = null; 93 | } 94 | 95 | // Update? 96 | update(ext: extension) { 97 | if (this.builderNode) { 98 | try { 99 | this.builderNode.update(ext); 100 | console.log(`[WebNode][update] ext.storage.get('my_window_id')=${ext.storage?.get('my_window_id') as string}`); 101 | } catch (e) { 102 | console.error(`[WebNode][update] Failed: ${e}!`); 103 | } 104 | } 105 | } 106 | 107 | dispose() { 108 | if (this.builderNode) { 109 | this.builderNode.dispose(); 110 | console.log(`[WebNode][dispose] OK!`); 111 | } else { 112 | console.warn(`[WebNode][dispose] Failed!`); 113 | } 114 | } 115 | } 116 | 117 | export const createEWeb = (uiContext: UIContext, tab: extension) => { 118 | let builderNode: BuilderNode<[extension]> = new BuilderNode(uiContext); 119 | builderNode.build(wrapBuilder<[extension]>(WebBuilder), tab); 120 | return builderNode; 121 | } -------------------------------------------------------------------------------- /home/src/main/ets/hosts/bunch_of_extensions.ets: -------------------------------------------------------------------------------- 1 | import { locale_messages_of_extension, manifest_of_extension, message_of_manifest_key } from '../processes/extension_actions'; 2 | import { webview } from '@kit.ArkWeb'; 3 | import { CatsBridge } from '../objects/CatstensionBridgeObj'; 4 | import { ExtNodeController } from '../objects/ExtWebNode'; 5 | 6 | export class bunch_of_extensions { 7 | extensions: extension[] = []; 8 | last_update: number = 0; 9 | NodeControllers: Array = []; 10 | 11 | update_timestamp() { 12 | this.last_update = Date.now(); 13 | } 14 | 15 | /** 16 | * Returns the extension object of a specific ID. 17 | * @param id The id. 18 | * @returns The extension object if found, undefined otherwise. 19 | * */ 20 | get_extension(id: string) { 21 | for (let index = 0; index < this.extensions.length; index++) { 22 | if (this.extensions[index].id == id) { 23 | return this.extensions[index]; 24 | } 25 | } 26 | return undefined; 27 | } 28 | 29 | index_of_id(id: string) { 30 | for (let index = 0; index < this.extensions.length; index++) { 31 | if (this.extensions[index].id == id) { 32 | return index; 33 | } 34 | } 35 | return undefined; 36 | } 37 | } 38 | 39 | export class extension { 40 | id: string; 41 | path: string; 42 | path_full: string; 43 | // Web 44 | controller: WebviewController = new webview.WebviewController(); 45 | bridge: CatsBridge; 46 | storage: LocalStorage | undefined; 47 | is_being_viewed: boolean = false; 48 | // Properties 49 | name: string; 50 | manifest_version: number; 51 | version: string; 52 | description: string | undefined; 53 | default_locale: string | undefined; 54 | homepage_url: string | undefined; 55 | // icon 56 | icon_path: string | undefined; 57 | // action 58 | action_default_icon_path: string | undefined; 59 | action_default_title: string | undefined; 60 | action_default_popup: string | undefined; 61 | // Objects 62 | manifest: object | undefined; 63 | messages: object | undefined; 64 | 65 | constructor(id: string, path: string, path_full: string) { 66 | this.id = id; 67 | this.path = path; 68 | this.path_full = path_full; 69 | this.manifest = manifest_of_extension(path); 70 | this.manifest_version = this.manifest?.['manifest_version']; 71 | this.name = this.manifest?.['name']; 72 | this.version = this.manifest?.['version']; 73 | this.description = this.manifest?.['description']; 74 | 75 | // icon 76 | let icons: object | undefined = this.manifest?.['icons']; 77 | this.icon_path = getMaxKeyPath(icons); 78 | 79 | // action 80 | let action_json_obj: object | undefined = this.manifest?.['action']; 81 | if (action_json_obj) { 82 | // icon 83 | let default_icon: object | undefined = action_json_obj?.['default_icon']; 84 | if (default_icon) { 85 | let best_path = getMaxKeyPath(default_icon); 86 | this.action_default_icon_path = best_path; 87 | } 88 | // default_title 89 | this.action_default_title = action_json_obj?.['default_title']; 90 | this.action_default_popup = action_json_obj?.['default_popup']; 91 | } 92 | 93 | // homepage_url 94 | this.homepage_url = this.manifest?.['homepage_url']; 95 | 96 | // i18n 97 | this.default_locale = this.manifest?.['default_locale']; 98 | if (this.default_locale) { 99 | // Do i18n 100 | this.messages = locale_messages_of_extension(path, this.default_locale); 101 | if (this.messages) { 102 | // Update manifest information 103 | this.name = message_of_manifest_key(this.messages, this.name); 104 | if (this.description) { 105 | this.description = message_of_manifest_key(this.messages, this.description); 106 | } 107 | if (this.action_default_title) { 108 | this.action_default_title = message_of_manifest_key(this.messages, this.action_default_title); 109 | } 110 | } 111 | } 112 | 113 | this.bridge = new CatsBridge(this); 114 | 115 | console.log(`[extension] Created! path=${path}, path_full=${path_full}`); 116 | } 117 | } 118 | 119 | /** 120 | * Finds the icon with largest size. 121 | * @param obj The default_icon object. 122 | * @returns A string, path of that icon. 123 | * */ 124 | function getMaxKeyPath(obj: object | undefined): string | undefined { 125 | if (!obj) { 126 | return; 127 | } 128 | 129 | if (typeof obj === 'string') { 130 | return obj as string; 131 | } 132 | 133 | const keys = Object.keys(obj); 134 | if (keys.length === 0) { 135 | return undefined; 136 | } 137 | console.log(`[getMaxKeyPath] keys=${keys},`) 138 | 139 | let maxKey = -Infinity; 140 | for (let i = 0; i < keys.length; i++) { 141 | const keyNum = parseInt(keys[i], 10); 142 | if (keyNum > maxKey) { 143 | maxKey = keyNum; 144 | } 145 | } 146 | 147 | return obj?.[maxKey.toString()]; 148 | } -------------------------------------------------------------------------------- /home/src/main/ets/utils/drag_drop_tools.ets: -------------------------------------------------------------------------------- 1 | import { unifiedDataChannel, uniformTypeDescriptor } from '@kit.ArkData'; 2 | import lazy { copy_from_uri_to_sandbox_temp_with_timestamp } from './storage_tools'; 3 | import lazy { storage_of_index, window_index_of_id } from './ui_tools'; 4 | 5 | /** 6 | * Pass a DragEvent and some of the contents of correct type would be added to meowScratchingBoard 7 | * @param e DragEvent 8 | * */ 9 | export function drop_to_scratching_board(e: DragEvent, storage: LocalStorage, openLinks: boolean) { 10 | let drop_ok = false; 11 | try { 12 | let drop_data_records = e.getData().getRecords(); 13 | let result_list_string: string[] = []; 14 | let last_link: string = ''; // For comparision and avoid opening duplicate links!~ 15 | for (let i = 0; i < drop_data_records.length; i++) { 16 | let record = drop_data_records[i]; 17 | console.log('[Meow][drop_to_scratching_board][record.getType()][' + i + '] ' + record.getType()); 18 | 19 | if (record.getType() == uniformTypeDescriptor.UniformDataType.HYPERLINK) { 20 | let desc = (record as unifiedDataChannel.Hyperlink).description; 21 | let url = (record as unifiedDataChannel.Hyperlink).url; 22 | 23 | if (openLinks) { 24 | // Directly open 25 | if (url != last_link) { 26 | storage.set('universal_new_tab_gateway', [url, false]); 27 | console.log("[drop_to_scratching_board] Hyperlink url: [" + url + "] Opened!"); 28 | } else { 29 | console.log("[drop_to_scratching_board] Hyperlink url: [" + url + "] Duplication Avoided!"); 30 | } 31 | last_link = url; 32 | } else { 33 | // Push Hyperlink 34 | if (desc && !result_list_string.includes(desc)) { 35 | // Push link description 36 | result_list_string.push(desc); 37 | drop_ok = true; 38 | } 39 | if (!result_list_string.includes(url)) { 40 | // Push link 41 | result_list_string.push(url); 42 | drop_ok = true; 43 | } 44 | console.log("[drop_to_scratching_board] Hyperlink url: [" + url + "]"); 45 | } 46 | } else if (record.getType() == uniformTypeDescriptor.UniformDataType.PLAIN_TEXT) { 47 | // Push text 48 | let textContent = (record as unifiedDataChannel.PlainText).textContent; 49 | if (!result_list_string.includes(textContent)) { 50 | result_list_string.push(textContent); 51 | drop_ok = true; 52 | } 53 | console.log("[drop_to_scratching_board] Plain Text: [" + textContent + "]"); 54 | 55 | } else if (record.getType() == uniformTypeDescriptor.UniformDataType.IMAGE) { 56 | // Push Image 57 | let image_uri = (record as unifiedDataChannel.Image).imageUri; 58 | console.log("[drop_to_scratching_board] image uri: [" + image_uri + "]"); 59 | try { 60 | result_list_string.push(copy_from_uri_to_sandbox_temp_with_timestamp(image_uri, 'web-drag-image-cache')); 61 | drop_ok = true; 62 | } catch (e) { 63 | console.error('[drop_to_scratching_board] Plan A of Image drop failed: ' + e); 64 | } 65 | 66 | } else if (record.getType() == 'ApplicationDefinedType') { 67 | let re = record as unifiedDataChannel.ApplicationDefinedRecord; 68 | if (re.applicationDefinedType == 'ApplicationDefinedTabDrag') { 69 | try { 70 | let data = Array.from(re.rawData); 71 | let from_window = data[0]; 72 | let from_tab = data[1]; 73 | let from_storage = storage_of_index(data[0]); 74 | let to_window = window_index_of_id(storage.get('my_window_id') as string); 75 | console.log('[drop_to_scratching_board] Got a tab! window_index = ' + from_window + ', tab_index = ' + from_tab + ', to_window_index = ' + to_window + '.'); 76 | // Move tab, notify from window. 77 | if (from_window == to_window) { 78 | // Moving a tab in back into its home window. This is meaningless so DO NOTHING. 79 | } else { 80 | from_storage.set('universal_move_tab_gateway_target', to_window); 81 | from_storage.set('universal_move_tab_gateway', data[1]); 82 | } 83 | } catch (e) { 84 | console.error('[drop_to_scratching_board] Tab drop failed: ' + e); 85 | } 86 | } 87 | } else { 88 | console.log(`[drop_to_scratching_board] Unrecognized data type!`); 89 | } 90 | } 91 | // Append to start of original scratching board content 92 | let original_drop_result_strings = storage.get('drop_result_strings') as string[]; 93 | storage.set('drop_result_strings', result_list_string.concat(original_drop_result_strings)); 94 | } catch (e) { 95 | console.error('[drop_to_scratching_board] Failed: ' + e); 96 | } 97 | e.setResult(DragResult.DRAG_SUCCESSFUL); 98 | return drop_ok; 99 | } 100 | -------------------------------------------------------------------------------- /home/src/main/ets/dialogs/contents/woofRecentFaultLogs.ets: -------------------------------------------------------------------------------- 1 | import { FaultLogger } from '@kit.PerformanceAnalysisKit'; 2 | // TODO: switch to https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/hiappevent-watcher-crash-events-arkts 3 | import { BusinessError } from '@kit.BasicServicesKit'; 4 | import lazy woofControlFrame from '../woofControlFrame'; 5 | import { copy } from '../../utils/clipboard_tools'; 6 | import linysCapsuleButton from '../../components/buttons/linysCapsuleButton'; 7 | import linysTextTitle from '../../components/texts/linysTextTitle'; 8 | import { animation_default } from '../../hosts/bunch_of_defaults'; 9 | import linysTextArea from '../../components/texts/linysTextArea'; 10 | 11 | @CustomDialog 12 | struct woofRecentFaultLogs { 13 | controller: CustomDialogController; 14 | @State faultLogs: string[] = []; 15 | @LocalStorageLink('opened_dialog_controllers') opened_dialog_controllers: CustomDialogController[] = []; 16 | 17 | aboutToAppear(): void { 18 | this.opened_dialog_controllers.push(this.controller); 19 | console.log(`[Dialog][Open] Now dialog count: ${this.opened_dialog_controllers.length}!`); 20 | } 21 | 22 | aboutToDisappear(): void { 23 | this.opened_dialog_controllers.splice(this.opened_dialog_controllers.indexOf(this.controller), 1); 24 | console.log(`[Dialog][Close] Now dialog count: ${this.opened_dialog_controllers.length}!`); 25 | } 26 | 27 | build() { 28 | woofControlFrame({ 29 | title: $r('app.string.Fault_logs'), 30 | controller: this.controller, 31 | space_column: 5 32 | }) { 33 | Scroll() { 34 | Column({ space: 10 }) { 35 | if (this.faultLogs.length == 0) { 36 | linysTextTitle({ 37 | text: $r('app.string.Fault_logs_empty') 38 | }) 39 | linysTextTitle({ 40 | text: ' ¯\\_(ツ)_/¯ ' 41 | }) 42 | .opacity(0.7) 43 | } else { 44 | ForEach(this.faultLogs, (fault: string, index: number) => { 45 | faultItem({ 46 | fault: fault, 47 | idx: index 48 | }) 49 | }) 50 | } 51 | } 52 | .alignItems(HorizontalAlign.Center) 53 | } 54 | .borderRadius(13.5) 55 | .layoutWeight(1) 56 | .scrollable(ScrollDirection.Vertical) 57 | .edgeEffect(EdgeEffect.Spring) 58 | .onAppear(() => { 59 | this.get_fault_logs(); 60 | }) 61 | } 62 | } 63 | 64 | get_fault_logs() { 65 | this.faultLogs = []; 66 | try { 67 | FaultLogger.query(FaultLogger.FaultType.JS_CRASH).then(value => { 68 | if (value) { 69 | console.info("FaultLog length is " + value.length); 70 | let len: number = value.length; 71 | for (let i = 0; i < len; i++) { 72 | let this_fault: string[] = []; 73 | // this_fault.push("log: " + i) 74 | this_fault.push("Log pid: " + value[i].pid) 75 | this_fault.push("Log uid: " + value[i].uid) 76 | this_fault.push("Log type: " + value[i].type) 77 | this_fault.push("Log timestamp: " + value[i].timestamp + 78 | " (" + new Date(value[i].timestamp).toLocaleString() + ")") 79 | this_fault.push("Log reason: " + value[i].reason) 80 | this_fault.push("Log module: " + value[i].module) 81 | // this_fault.push("Log summary: " + value[i].summary) 82 | this_fault.push("Log text: " + value[i].fullLog) 83 | this.faultLogs.push(this_fault.join("\n")); 84 | } 85 | } 86 | }); 87 | } catch (err) { 88 | console.error(`code: ${(err as BusinessError).code}, message: ${(err as BusinessError).message}`); 89 | } 90 | } 91 | } 92 | 93 | export default woofRecentFaultLogs; 94 | 95 | @Component 96 | struct faultItem { 97 | @Prop fault: string = 'meow meow meow wait a second meow!'; 98 | @Prop idx: number; 99 | // Colors 100 | @StorageLink('color_current_font') color_current_font: ResourceColor = $r('app.color.font_color_title'); 101 | // Edit 102 | @State copied: boolean = false; 103 | // Settings / Accessibility 104 | @StorageLink('preferred_hand_left_or_right') preferred_hand_left_or_right: string = 'right'; 105 | 106 | build() { 107 | Column({ space: 10 }) { 108 | Row({ space: 10 }) { 109 | Row() { 110 | linysTextTitle({ text: (this.idx + 1).toString() + "." }) 111 | } 112 | .layoutWeight(this.preferred_hand_left_or_right == 'right' ? 1 : undefined) 113 | .animation(animation_default()) 114 | 115 | linysCapsuleButton({ 116 | text: this.copied ? " 󰆊 " : " 󰆝 " 117 | }) 118 | .onClick(() => { 119 | this.copy_this(); 120 | }) 121 | } 122 | .alignItems(VerticalAlign.Bottom) 123 | .width('100%') 124 | 125 | linysTextArea({ text: this.fault, init_text: this.fault }) 126 | } 127 | } 128 | 129 | copy_this() { 130 | this.copied = true; 131 | copy(this.fault); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /home/src/main/ets/utils/color_tools.ets: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts a hsv color into rgb style. 3 | * @param h A number, the hue. 4 | * @param s A number, the saturation. 5 | * @param v A number, the value (of brightness). 6 | * @returns A number[] array, [r, g, b]. 7 | * */ 8 | export function hsv2rgb(h: number, s: number, v: number): number[] { 9 | h = Math.max(0, Math.min(360, h)); 10 | s = Math.max(0, Math.min(100, s)) / 100; 11 | v = Math.max(0, Math.min(100, v)) / 100; 12 | 13 | const i = Math.floor(h / 60); 14 | const f = (h / 60) - i; 15 | const p = v * (1 - s); 16 | const q = v * (1 - f * s); 17 | const t = v * (1 - (1 - f) * s); 18 | 19 | let r: number = 0, g: number = 0, b: number = 0; 20 | 21 | switch (i % 6) { 22 | case 0: 23 | r = v; 24 | g = t; 25 | b = p; 26 | break; 27 | case 1: 28 | r = q; 29 | g = v; 30 | b = p; 31 | break; 32 | case 2: 33 | r = p; 34 | g = v; 35 | b = t; 36 | break; 37 | case 3: 38 | r = p; 39 | g = q; 40 | b = v; 41 | break; 42 | case 4: 43 | r = t; 44 | g = p; 45 | b = v; 46 | break; 47 | case 5: 48 | r = v; 49 | g = p; 50 | b = q; 51 | break; 52 | } 53 | 54 | r = Math.round(r * 255); 55 | g = Math.round(g * 255); 56 | b = Math.round(b * 255); 57 | 58 | return [r, g, b]; 59 | } 60 | 61 | /** 62 | * Converts an rgb style color into hex format. 63 | * @param r A number, standing for the red value of the original rgb color. 64 | * @param g A number, standing for the green value of the original rgb color. 65 | * @param b A number, standing for the blue value of the original rgb color. 66 | * @returns A string, the hex color. Like #rrggbb. 67 | * */ 68 | export function rgb2hex(r: number, g: number, b: number) { 69 | let hex = ("#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)).toUpperCase(); 70 | return hex; 71 | } 72 | 73 | /** 74 | * Converts a hsv number style color into hex format. 75 | * @param hsv A number[] array, [hue, saturation, value] standing for the hsv color in number style. 76 | * @returns A string, the hex color. Like #rrggbb. 77 | * */ 78 | export function hsv2hex(hsv: number[]) { 79 | let rgb: number[] = hsv2rgb(hsv[0], hsv[1], hsv[2]); 80 | let hex: string = rgb2hex(rgb[0], rgb[1], rgb[2]); 81 | return hex; 82 | } 83 | 84 | /** 85 | * Converts a hex string color to rgb format. 86 | * @param hex A string, in the format of #______. 87 | * @returns [r, g, b] if success. 88 | * */ 89 | export function hex2rgb(hex: ResourceColor) { 90 | hex = (hex as string).toLowerCase(); 91 | let rgb: number[] = []; 92 | for (let i = 1; i < 7; i += 2) { 93 | rgb.push(parseInt('0x' + hex.slice(i, i + 2))); 94 | } 95 | return rgb; 96 | } 97 | 98 | /** 99 | * Converts an rgb format color to hsv values. 100 | * @param r A number, standing for the red value of the original rgb color. 101 | * @param g A number, standing for the green value of the original rgb color. 102 | * @param b A number, standing for the blue value of the original rgb color. 103 | * @returns A number[] array, [h, s, v]. 104 | * */ 105 | export function rgb2hsv(r: number, g: number, b: number) { 106 | r = Math.max(0, Math.min(255, r)); 107 | g = Math.max(0, Math.min(255, g)); 108 | b = Math.max(0, Math.min(255, b)); 109 | 110 | const rPrime = r / 255; 111 | const gPrime = g / 255; 112 | const bPrime = b / 255; 113 | 114 | const max = Math.max(rPrime, gPrime, bPrime); 115 | const min = Math.min(rPrime, gPrime, bPrime); 116 | 117 | const v = max; 118 | 119 | let s: number; 120 | if (max !== 0) { 121 | s = (max - min) / max; 122 | } else { 123 | s = 0; 124 | } 125 | 126 | let h: number; 127 | const delta = max - min; 128 | if (delta !== 0) { 129 | if (rPrime === max) { 130 | h = (gPrime - bPrime) / delta; 131 | } else if (gPrime === max) { 132 | h = 2 + (bPrime - rPrime) / delta; 133 | } else { 134 | // bPrime === max 135 | h = 4 + (rPrime - gPrime) / delta; 136 | } 137 | 138 | h = Math.round(h * 60); 139 | 140 | if (h < 0) { 141 | h += 360; 142 | } 143 | } else { 144 | h = 0; 145 | } 146 | 147 | return [h, Math.round(s * 100), Math.round(v * 100)]; 148 | } 149 | 150 | /** 151 | * Converts a hex string color to hsv values. 152 | * @param hex A string, the hex color. 153 | * @returns A number[] array, [h, s, v]. 154 | * */ 155 | export function hex2hsv(hex: ResourceColor) { 156 | let rgb: number[] = hex2rgb(hex as string); 157 | let hsv = rgb2hsv(rgb[0], rgb[1], rgb[2]); 158 | return hsv; 159 | } 160 | 161 | /** 162 | * Examines a string and check if it is a legal #______ hex color. 163 | * @param hex A string. 164 | * */ 165 | export function is_legal_hex(hex: ResourceColor) { 166 | let digits: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']; 167 | hex = (hex as string).toUpperCase(); 168 | if (hex[0] != "#") { 169 | return false; 170 | } 171 | if (hex.length != 7) { 172 | return false; 173 | } 174 | for (let index = 1; index < hex.length; index++) { 175 | if (!digits.includes(hex[index])) { 176 | return false; 177 | } 178 | } 179 | return true; 180 | } 181 | 182 | /** 183 | * Adds transparency (0-255) to a ResourceColor 184 | * */ 185 | export function add_transparency(color: ResourceColor, transparency: number) { 186 | let transparency_hex = ((255 - transparency) & 0xff).toString(16).padStart(2, '0'); 187 | return '#' + transparency_hex + (color as string).substring(1); 188 | } 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liny 的冲浪喵! 2 | 3 | [中文](README.md) | [English](README_EN.md) 4 | 5 | ## 一些前话 6 | 7 | 这个项目的诞生起初的目的是为 Liny 自己升级 HarmonyOS NEXT 之后创造一个容身之处, 8 | 但是发现似乎大家都需要这么一个玩意,于是就开源出来给大家用了。 9 | 10 | 但是由于 Liny 自己事务安排的缘故,查看 Issue、更新可能都不太及时, 11 | 很多事情有时候都比较滞后,还请各位见谅。 12 | 13 | 如果你见到上一个 Commit 是在好几个星期、甚至几个月前, 14 | 也请不要轻易地认为这个项目死掉了, 15 | 因为 Liny 可能正好在这段时间被迫沉迷写论文(bushi)而无暇关注这边的进度。 16 | 并且现在 DevEco Studio 并没有推出原生 HarmonyOS 版本, 17 | 由于卡到飞起的 Windows 模拟器 x86 转译,这个项目的更新可能更加缓慢了(:O) 18 | 19 | 然后,解决拖更问题的最好办法其实是给这个项目 **交一个 Issue**, 20 | 这样 Liny 就会感到十分焦虑,就会决定先来更新了。 21 | 22 | 让我们一起说:谢谢 Issue! 23 | 24 | :3 25 | 26 | ## 快速上手 27 | 28 | 在 [build_auto](build_auto) 目录下有最新测试版本的 build 产品, 29 | 这些产品可能存在某些未发现的不稳定因素,不建议长期使用 30 | ——用了的话,记得常来检查更新! 31 | 32 | ~~尽管很多时候开发版比稳定版稳定……~~ 33 | 34 | 在界面下方的输入框里输入 URL 或关键词,并且回车或点击小放大镜以提交。 35 | 36 | 点击四个小圆点(2x2)组成的按钮以查看页面详情和更多功能,再点一次以关闭面板。 37 | 38 | ## 启发 39 | 40 | Liny 的浏览器为 HarmonyOS NEXT 而构建,旨在为各种性能水平的设备提供一个浏览器的轻量之选。 41 | 42 | 这个项目受 tuyafeng 大佬传奇之作 [Via](https://viayoo.com/)、 43 | 来自 Mozilla 的最可爱赛博小狐狸 [Firefox](https://firefox.com/) 之启发, 44 | 以及来自开源鸿蒙的 [浏览器](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Web/Browser) 45 | 及其定制增强版分支 [🌐 浏览器CE(社区版)](https://gitee.com/westinyang/browser-ce) 46 | 开放代码的帮助(解惑),最终得以呈现。感谢你们!!(* ̄3 ̄)╭ 47 | 48 | ## 预览 49 | 50 | 手机: 51 | 52 | | 首页(浅) | 设置(浅) | 53 | |:------------------------------------------:|:--------------------------------------------:| 54 | | ![gallery_9.jpg](build_auto/gallery_9.jpg) | ![gallery_10.jpg](build_auto/gallery_10.jpg) | 55 | 56 | | 首页(深) | 设置(深) | 57 | |:------------------------------------------:|:------------------------------------------:| 58 | | ![gallery_3.jpg](build_auto/gallery_3.jpg) | ![gallery_4.jpg](build_auto/gallery_4.jpg) | 59 | 60 | 折叠屏: 61 | 62 | | 首页(浅) | 设置(浅) | 63 | |:------------------------------------------:|:------------------------------------------:| 64 | | ![gallery_7.jpg](build_auto/gallery_7.jpg) | ![gallery_8.jpg](build_auto/gallery_8.jpg) | 65 | 66 | | 首页(深) | 设置(深) | 67 | |:------------------------------------------:|:------------------------------------------:| 68 | | ![gallery_1.jpg](build_auto/gallery_1.jpg) | ![gallery_2.jpg](build_auto/gallery_2.jpg) | 69 | 70 | 平板: 71 | 72 | | 首页(浅) | 设置(浅) | 73 | |:--------------------------------------------:|:--------------------------------------------:| 74 | | ![gallery_11.jpg](build_auto/gallery_11.jpg) | ![gallery_12.jpg](build_auto/gallery_12.jpg) | 75 | 76 | | 首页(深) | 设置(深) | 77 | |:------------------------------------------:|:------------------------------------------:| 78 | | ![gallery_5.jpg](build_auto/gallery_5.jpg) | ![gallery_6.jpg](build_auto/gallery_6.jpg) | 79 | 80 | 注意!颜色和背景需要自己设定!:P 81 | 82 | ## 相关权限 83 | 84 | [ohos.permission.INTERNET](https://gitee.com/openharmony/docs/blob/master/zh-cn/application-dev/security/permission-list.md#ohospermissioninternet) 85 | 86 | ## 计划和开发 87 | 88 | 可能需要很多时间,也可能有些东西会永远被搁置。(ง •_•)ง 89 | 90 | ### 功能 91 | 92 | - [x] 多窗口/多实例模式 93 | - [x] 在窗口间移动标签页 94 | - [x] Cookies 管理与清理 95 | - [x] 缓存管理与清理 96 | - [x] 广告过滤+EasyList 97 | - [x] 分享/复制网址 98 | - [ ] 分布式能力同步数据(历史记录、书签) 99 | - [ ] 历史记录要求生物识别访问 100 | - [ ] 书签页面要求生物识别访问 101 | - [x] 打开 HTML 文件 102 | - [x] 历史记录搜索 103 | - [x] 自定义首页背景 104 | - [x] 自定义首页网址快捷方式 105 | - [x] 下载选项自定义:每次弹窗询问 106 | - [x] 直接下载到系统下载目录 107 | - [x] 页面内搜索 108 | - [x] 禁用 JavaScript 109 | - [x] 无图模式 110 | - [ ] 隐私模式 111 | - [ ] 网页链接右键/长按菜单(预览、操作) 112 | - [ ] 阅读模式 113 | - [ ] 搜索建议 114 | - [ ] 插件/脚本 115 | - [ ] DRM 内容支持 116 | - [x] 响应网页的新标签页打开要求 117 | - [x] 网页内下载功能 118 | - [x] UA 切换与自定义(UA 相关设置) 119 | - [x] 启动时恢复上次浏览的进度 120 | - [x] 多任务平行浏览 121 | 122 | ### 体验 123 | 124 | - [x] 自定义主题(配色) 125 | - [x] 标题栏自定义上下 126 | - [x] 标签页栏自定义横竖 127 | - [x] 接入系统返回 128 | - [x] 客制化动画曲线 129 | - [ ] 更高级的鸿蒙风格视觉效果 130 | 131 | ## 约束和限制 132 | 133 | ### 道阻且长。 134 | 135 | 1. 由于 Liny 不成体系的编程思想和低陋的代码水平,这个应用可能存在一些性能问题。 136 | 如果出现错误,还请各路前辈高手斧正,感激不尽! 137 | 2. 对于某些工作区 / 恢复的标签页,有时会发生 cppcrash。(调查中) 138 | 3. Cloudflare 人机验证可能无法通过。(使用 Android UA 就可以了!) 139 | 4. 由于神秘的问题和暂时却不得不的妥协, 140 | 有时候启动应用后广告屏蔽白名单可能生效不及时,导致网页上的广告还是被屏蔽了。 141 | (正在想办法解决) 142 | 5. 由于神秘的问题和暂时却不得不的妥协, 143 | 有时候启动应用后广告屏蔽启用可能生效不及时,导致网页上的广告没有被屏蔽。 144 | (正在想办法解决) 145 | 6. HarmonyOS UA 不被很多网站所理解,并且常常会导致不正确的网站呈现(但是可以自定义 UA 了)。 146 | 7. 这个项目要求 HarmonyOS NEXT API20 以运行。 147 | 8. OHOS 没有 Share Kit 怎么办(?)在线等,急。 148 | 149 | ### 似乎解决了? 150 | 151 | 1. 也许是因为 Windows x86-64 模拟器的性能问题,快速打开大量新标签页会导致错误 17100001 152 | (Init error. The WebviewController must be associated with a Web component) 153 | 并且使应用崩溃。 154 | (似乎解决了) 155 | 2. 快速关闭很多标签页似乎会导致所有的标签页按钮消失。 156 | 这是因为在某些未知的特定条件下,标签页按钮会尝试关闭 最后+1 个标签页面。 157 | 一个判断语句已经被添加,用于检测请求关掉的页面是否合法。 158 | (似乎解决了) 159 | 3. 神秘的问题导致长期不用之后启动会闪退 → 160 | Issue [#40](https://github.com/awaLiny2333/LinysBrowser_NEXT/issues/40) 161 | (似乎解决了)。 162 | 4. 神秘的问题导致快捷键修改之后没有生效? 163 | (似乎解决了,但是需要重启才能把原来的解绑掉) 164 | 5. 神秘问题导致应用启动后第一次打开设置页面会发现某些分段按钮不显示已选择的项。 165 | (似乎消失了) 166 | 6. 在某些情况下打开上次关闭时的标签页功能会失败,于是什么标签页都没有显示出来。 167 | (似乎解决了) 168 | 169 | ## 提交信息的说明 170 | 171 | 这个项目诞生以来的提交信息过于混乱,于是 Liny 决定作出一些自我约束…… 172 | 173 | 1. fix:修复更新,用于标注对一个问题的修复; 174 | 2. feat:功能更新,一般用于标注推出了什么新功能; 175 | 3. improve:改进更新,一般用于标注已有的某些东西的变化,如逻辑、界面、文案等; 176 | 4. code:代码更新,用于标注代码优化,如代码整理、逻辑整理等; 177 | 5. version:版本号变更,用于标注版本号的变化; 178 | 6. api:API 级别变更,用于标注需要的 API 级别的变化; 179 | 7. other: 未归类的其它提交。 -------------------------------------------------------------------------------- /home/src/main/ets/workers/History_indexer.ets: -------------------------------------------------------------------------------- 1 | import { collections, ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; 2 | import { sandbox_save } from '../utils/storage_tools'; 3 | import { fileIo as fs } from '@kit.CoreFileKit'; 4 | import { bunch_of_history_index_lite as host } from '../hosts/bunch_of_history_index_lite'; 5 | 6 | const workerPort: ThreadWorkerGlobalScope = worker.workerPort; 7 | 8 | /** 9 | * Defines the event handler to be called when the worker thread receives a message sent by the host thread. 10 | * The event handler is executed in the worker thread. 11 | * 12 | * @param event message data 13 | */ 14 | workerPort.onmessage = (event: MessageEvents) => { 15 | // receive getContext().filesDir from main thread 16 | // history_index_host = event.data; 17 | if (typeof event.data == "string") { 18 | let filesDir = event.data as string; 19 | // Execution 20 | rebuild_full_history_index_sync(filesDir); 21 | // Report done 22 | workerPort.postMessage('done'); 23 | } 24 | }; 25 | 26 | /** 27 | * Defines the event handler to be called when the worker receives a message that cannot be deserialized. 28 | * The event handler is executed in the worker thread. 29 | * 30 | * @param event message data 31 | */ 32 | workerPort.onmessageerror = (event: MessageEvents) => { 33 | }; 34 | 35 | /** 36 | * Defines the event handler to be called when an exception occurs during worker execution. 37 | * The event handler is executed in the worker thread. 38 | * 39 | * @param event error message 40 | */ 41 | workerPort.onerror = (event: ErrorEvent) => { 42 | }; 43 | 44 | 45 | /** 46 | * Builds index for all history and saves (overwrites) the index file onto disk. 47 | * 48 | * THIS IS A SYNC FUNCTION 49 | * */ 50 | function rebuild_full_history_index_sync(context_filesDir: string) { 51 | let s = Date.now(); 52 | console.log(host.log_head_worker() + ' Start rebuilding full index! Start: ' + new Date(s).toString() + '.') 53 | 54 | // Clear all previous index 55 | try { 56 | fs.rmdirSync(context_filesDir + "/history-index"); 57 | fs.mkdirSync(context_filesDir + "/history-index", true); 58 | } catch (e) { 59 | 60 | } 61 | 62 | // Get all months 63 | let months = get_history_months_no_create_this_month(context_filesDir); 64 | 65 | // Read month by month 66 | for (let index = 0; index < months.length; index++) { 67 | host.clear(); 68 | 69 | // Index 70 | const year_month = months[index]; 71 | // bunch_of_history_index.add_index_history_file(file_path, context_filesDir); 72 | host.index_from_history_month_file(year_month[0], year_month[1], context_filesDir); 73 | 74 | // Save to disk 75 | console.log(host.log_head_worker() + ' Indexing month: ' + year_month.toString() + '. Map size: ' + host.index_map.size.toString() + '.'); 76 | 77 | let month_field: string = year_month[1].toString(); 78 | if (year_month[1].toString().length == 1) { 79 | // is a single digit 80 | // This works until human have over 99 months a year, perhaps. 81 | month_field = "0" + year_month[1].toString(); 82 | } 83 | save_index_ext("index_" + year_month[0] + "_" + month_field + "_00_00_00_00_000.txt", host.index_map, context_filesDir); 84 | 85 | // Report status 86 | const progress = index / months.length * 100; 87 | workerPort.postMessage((Date.now() - s).toString() + " ms, " + progress.toFixed(2) + "%"); 88 | // console.log("[Meow][bunch_of_history_index] Index progress: " + progress.toFixed(2) + "%"); 89 | } 90 | 91 | // save indexer version for future use 92 | // perhaps? 93 | sandbox_save('last_indexer_version.txt', "1", context_filesDir); 94 | 95 | // Timer & logs 96 | console.log(host.log_head_worker() + ' Finish full index! Time used: ' + ((Date.now() - s) / 1000).toString() + "s.") 97 | workerPort.postMessage((Date.now() - s).toString() + " ms, 100%"); 98 | } 99 | 100 | // Functions borrowed from bunch_of_history 101 | // to avoid error when bunch_of_history is not even initialized 102 | // TODO: Find a more elegant way of doing this 103 | 104 | /** 105 | * Lists all the months in which ther are history records made, in ascending order. 106 | * @returns A number[][] array, e.g. [[2023, 2], [2023, 3], [2023, 4], ...]. 107 | * */ 108 | function get_history_months_no_create_this_month(context_filesDir: string) { 109 | let filesDir: string = context_filesDir; 110 | let result: number[][] = []; 111 | let history_files = fs.listFileSync(filesDir + '/history', { recursion: false }); 112 | for (let index = 0; index < history_files.length; index++) { 113 | let f_name = history_files[index].split('.')[0]; 114 | // 'history_2024_02.txt' 115 | let f_split = f_name.split('_'); 116 | let year = parseInt(f_split[1]); 117 | let month = parseInt(f_split[2]); 118 | result.push([year, month]); 119 | } 120 | result = result.sort((a, b) => (a[0] * 12 + a[1]) - (b[0] * 12 + b[1])); 121 | console.log(host.log_head_worker() + ' Got_available_months. Result: ' + result.join(' ') + '.') 122 | return result; 123 | } 124 | 125 | /** 126 | * Saves index set to sandbox 127 | * @param index_name the file name of this index slice. For example, 'index_2023_02_01_23_55_11_020.txt'. 128 | * @param set 129 | * @param context_filesDir 130 | * */ 131 | function save_index_ext(index_name: string, set: collections.Map>, context_filesDir: string) { 132 | let filesDir: string = context_filesDir; 133 | let result: string[] = []; 134 | set.forEach((value, key) => { 135 | result.push(key) 136 | result.push(value.join("_")); 137 | }) 138 | // console.log("qwq " + result.toString()) 139 | sandbox_save('history-index/' + index_name, result.join('\n'), filesDir); 140 | } -------------------------------------------------------------------------------- /home/src/main/ets/utils/kv_store_tools.ets: -------------------------------------------------------------------------------- 1 | import { distributedKVStore } from '@kit.ArkData'; 2 | import { BusinessError } from '@kit.BasicServicesKit'; 3 | import { meowContext } from './environment_tools'; 4 | 5 | /** 6 | * THEORETICALLY THIS FILE IS LEGACY. 7 | * 8 | * AS WE HAVE MOVED TOTALLY AWAY FROM KVSTORE SINCE SEP 30 2025. 9 | * 10 | * BUT MIGRATION IN bunch_of_settings.ets AND BOOKMARKS (init.ets) STILL NEEDS THIS, SO WE KEPT THIS FILE INTACT. 11 | * */ 12 | 13 | let kvs: distributedKVStore.DeviceKVStore | undefined = undefined; 14 | 15 | export async function kv_store_get(key_name: string) { 16 | if (kvs == undefined) { 17 | await get_kv_store_into_app_storage(); 18 | // console.log("[kv_store_tools][GET] kvStore not found from AppStorage! Created & Stored a new one."); 19 | } else { 20 | // console.log('[kv_store_tools][GET] Using an existing KVStore.'); 21 | } 22 | 23 | if (kvs) { 24 | // If kvStore already got 25 | let get_result = ""; 26 | await kvs.get(key_name).then((value) => { 27 | // console.log("[kv_store_tools] Succeeded in getting data, key: " + key_name + ".") 28 | get_result = value as string; 29 | }).catch(() => { 30 | get_result = "undefined" 31 | }) 32 | return get_result; 33 | } else { 34 | let get_result = "error" 35 | console.error("[kv_store_tools][GET][ERROR] Undefined KVStore! ") 36 | return get_result; 37 | } 38 | } 39 | 40 | // export async function kv_store_put(key_name: string, put_content: string) { 41 | // if (kvs == undefined) { 42 | // await get_kv_store_into_app_storage(); 43 | // // console.log("[kv_store_tools][GET] kvStore not found from AppStorage! Created & Stored a new one."); 44 | // } else { 45 | // // console.log('[kv_store_tools][GET] Using an existing KVStore.'); 46 | // } 47 | // 48 | // if (kvs) { 49 | // kvs.put(key_name, put_content, (err) => { 50 | // if (err !== undefined) { 51 | // console.error(`[kv_store_tools][ERROR] Failed to put data. Code: ${err.code},message: ${err.message}`); 52 | // } else { 53 | // console.log("[kv_store_tools] Succeeded in putting data, key: " + key_name + ", content:\n\t" + put_content); 54 | // } 55 | // }); 56 | // } 57 | // 58 | // } 59 | // 60 | // export async function kv_store_delete(key_name: string) { 61 | // if (kvs == undefined) { 62 | // console.log("[kv_store_tools][GET] Launch of app, now getting kvStore") 63 | // await get_kv_store_into_app_storage(); 64 | // } 65 | // if (kvs) { 66 | // kvs.delete(key_name, (err) => { 67 | // if (err !== undefined) { 68 | // console.error(`[kv_store_tools][ERROR] Failed to delete data. Code: ${err.code},message: ${err.message}`); 69 | // } else { 70 | // console.info("[kv_store_tools] Succeeded in deleting data, key: " + key_name + "."); 71 | // } 72 | // }); 73 | // } 74 | // } 75 | 76 | async function get_kv_store_into_app_storage() { 77 | console.log('[kv_store_tools] Getting a KVStore into AppStorage...') 78 | let t0 = Date.now(); 79 | 80 | const kvManagerConfig: distributedKVStore.KVManagerConfig = { 81 | context: meowContext(), 82 | bundleName: 'meow.liny.browser.cat.uwu', 83 | } 84 | 85 | try { 86 | let kvManager = distributedKVStore.createKVManager(kvManagerConfig); 87 | console.info("[kv_store_tools] Succeeded in creating KVManager!"); 88 | // Get kvManager 89 | 90 | const options: distributedKVStore.Options = { 91 | createIfMissing: true, 92 | encrypt: false, 93 | backup: false, 94 | autoSync: false, 95 | kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION, 96 | securityLevel: distributedKVStore.SecurityLevel.S2, 97 | }; 98 | 99 | await kvManager.getKVStore('storeId', options) 100 | .then((store: distributedKVStore.DeviceKVStore) => { 101 | kvs = store; 102 | let t = Date.now() - t0; 103 | console.info("[kv_store_tools] Succeeded in getting KVStore! (" + t + ' ms)'); 104 | // Got kvStore 105 | }).catch((err: BusinessError) => { 106 | console.error(`[kv_store_tools][ERROR] Failed to get KVStore. Code is ${err.code},message is ${err.message}.`); 107 | }); 108 | // Get kvStore 109 | 110 | } catch (e) { 111 | let error = e as BusinessError; 112 | console.error(`[kv_store_tools][ERROR] Failed to create KVManager. Code is ${error.code},message is ${error.message}.`); 113 | } 114 | } 115 | 116 | // /** 117 | // * The only items stored in kv_store are the browsing statuses. 118 | // * @returns A json format string. 119 | // * */ 120 | // export async function export_json_kv_store() { 121 | // let result_string: string[] = []; 122 | // result_string.push(' "continue_tabs_count": "' + await kv_store_get('continue_tabs_count') + '"'); 123 | // result_string.push(' "continue_tabs_main_on": "' + await kv_store_get('continue_tabs_main_on') + '"'); 124 | // result_string.push(' "continue_tabs_sub_on": "' + await kv_store_get('continue_tabs_sub_on') + '"'); 125 | // result_string.sort(); 126 | // return '{\n' + result_string.join(',\n') + '\n}'; 127 | // } 128 | // 129 | // /** 130 | // * Imports settings from a json text. 131 | // * @param text The json text. 132 | // * @returns True if succeeded. 133 | // * */ 134 | // export async function import_json_kv_store(text: string) { 135 | // let jsonObject: Record | null = null; 136 | // if (text.length < 2) { 137 | // return false; 138 | // } 139 | // 140 | // try { 141 | // jsonObject = JSON.parse(text); 142 | // } catch (e) { 143 | // console.log('[Meow][kv_store_tools] Import kv_store failed! Json parse failed!'); 144 | // return false; 145 | // } 146 | // 147 | // Object.entries(jsonObject as Record).forEach(async (item) => { 148 | // const key = item[0]; 149 | // const val = item[1]; 150 | // kv_store_put(key, val); 151 | // }) 152 | // console.log('[Meow][kv_store_tools] Import kv_store:\n' + text); 153 | // return true; 154 | // } -------------------------------------------------------------------------------- /home/src/main/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "home", 4 | "type": "entry", 5 | "srcEntry": "./ets/homeabilitystage/HomeAbilityStage.ets", 6 | "description": "$string:module_desc", 7 | "mainElement": "HomeAbility", 8 | "deviceTypes": [ 9 | "default", 10 | "tablet", 11 | "2in1", 12 | // "wearable" 13 | ], 14 | "deliveryWithInstall": true, 15 | "installationFree": false, 16 | "pages": "$profile:main_pages", 17 | "abilities": [ 18 | { 19 | "name": "HomeAbility", 20 | "srcEntry": "./ets/homeability/HomeAbility.ets", 21 | "description": "$string:HomeAbility_desc", 22 | "icon": "$media:layered_image", 23 | "launchType": "specified", 24 | "label": "$string:HomeAbility_label", 25 | "startWindowIcon": "$media:startIcon", 26 | "startWindowBackground": "$color:start_window_background", 27 | "exported": true, 28 | "removeMissionAfterTerminate": true, 29 | "orientation": "auto_rotation_restricted", 30 | "continuable": true, 31 | "preferMultiWindowOrientation": "landscape_auto", 32 | "skills": [ 33 | { 34 | "entities": [ 35 | "entity.system.home", 36 | ], 37 | "actions": [ 38 | "action.system.home", 39 | ], 40 | "uris": [ 41 | { 42 | "type": "general.hyperlink", 43 | } 44 | ] 45 | }, 46 | { 47 | "entities": [ 48 | "entity.system.browsable" 49 | ], 50 | "actions": [ 51 | "ohos.want.action.viewData" 52 | ], 53 | "uris": [ 54 | { 55 | "scheme": "file", 56 | // Accept All Files 57 | "linkFeature": "FileOpen" 58 | }, 59 | { 60 | "scheme": "file", 61 | "type": "text/html", 62 | // Accept html 63 | "linkFeature": "FileOpen" 64 | }, 65 | { 66 | "scheme": "file", 67 | "type": "application/xhtml+xml", 68 | // Accept xhtml 69 | "linkFeature": "FileOpen" 70 | }, 71 | { 72 | "scheme": "file", 73 | "type": "message/rfc822", 74 | // Accept mht 75 | "linkFeature": "FileOpen" 76 | }, 77 | { 78 | "scheme": "https" 79 | }, 80 | { 81 | "scheme": "http" 82 | } 83 | ] 84 | } 85 | ], 86 | "minWindowHeight": 260, 87 | "minWindowWidth": 360 88 | } 89 | ], 90 | "extensionAbilities": [ 91 | { 92 | "name": "HomeBackupAbility", 93 | "srcEntry": "./ets/homebackupability/HomeBackupAbility.ets", 94 | "type": "backup", 95 | "exported": false, 96 | "metadata": [ 97 | { 98 | "name": "ohos.extension.backup", 99 | "resource": "$profile:backup_config" 100 | } 101 | ] 102 | } 103 | ], 104 | "requestPermissions": [ 105 | { 106 | "name": "ohos.permission.INTERNET", 107 | "reason": "$string:permission_reason_INTERNET", 108 | "usedScene": { 109 | "when": "inuse", 110 | "abilities": [ 111 | "HomeAbility" 112 | ] 113 | } 114 | }, 115 | { 116 | "name": "ohos.permission.GET_NETWORK_INFO", 117 | "reason": "$string:permission_reason_INTERNET", 118 | "usedScene": { 119 | "when": "inuse", 120 | "abilities": [ 121 | "HomeAbility" 122 | ] 123 | } 124 | }, 125 | { 126 | "name": "ohos.permission.PRINT", 127 | "reason": "$string:permission_reason_PRINT", 128 | "usedScene": { 129 | "when": "inuse", 130 | "abilities": [ 131 | "HomeAbility" 132 | ] 133 | } 134 | }, 135 | { 136 | "name": "ohos.permission.MICROPHONE", 137 | "reason": "$string:permission_reason_MICROPHONE", 138 | "usedScene": { 139 | "when": "inuse", 140 | "abilities": [ 141 | "HomeAbility" 142 | ] 143 | } 144 | }, 145 | { 146 | "name": "ohos.permission.CAMERA", 147 | "reason": "$string:permission_reason_CAMERA", 148 | "usedScene": { 149 | "when": "inuse", 150 | "abilities": [ 151 | "HomeAbility" 152 | ] 153 | } 154 | }, 155 | { 156 | "name": "ohos.permission.WINDOW_TOPMOST", 157 | "reason": "$string:permission_reason_WINDOW_TOPMOST", 158 | "usedScene": { 159 | "when": "inuse", 160 | "abilities": [ 161 | "HomeAbility" 162 | ] 163 | } 164 | }, 165 | { 166 | "name": "ohos.permission.DETECT_GESTURE", 167 | "reason": "$string:permission_reason_DETECT_GESTURES", 168 | "usedScene": { 169 | "when": "inuse", 170 | "abilities": [ 171 | "HomeAbility" 172 | ] 173 | } 174 | }, 175 | { 176 | "name": "ohos.permission.ACCESS_FIDO2_ONLINEAUTH", 177 | "reason": "$string:permission_reason_ACCESS_FIDO2_ONLINEAUTH", 178 | "usedScene": { 179 | "when": "inuse", 180 | "abilities": [ 181 | "HomeAbility" 182 | ] 183 | } 184 | }, 185 | // { 186 | // "name" : "ohos.permission.READ_PASTEBOARD", 187 | // "reason": "$string:permission_reason_READ_PASTEPOARD", 188 | // "usedScene": { 189 | // "when": "inuse", 190 | // "abilities": [ 191 | // "HomeAbility" 192 | // ] 193 | // } 194 | // } 195 | ] 196 | } 197 | } --------------------------------------------------------------------------------