├── .eslintrc.cjs ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── _locales ├── en │ └── messages.json └── zh_CN │ └── messages.json ├── manifest.json ├── package.json ├── postcss.config.js ├── public ├── icons │ ├── nut-mark-128x128.png │ ├── nut-mark-16x16.png │ ├── nut-mark-32x32.png │ ├── nut-mark-48x48.png │ ├── nut-unmark-128x128.png │ ├── nut-unmark-16x16.png │ ├── nut-unmark-32x32.png │ └── nut-unmark-48x48.png ├── install-in-chrome.png ├── install-in-chrome.svg ├── install-in-edge.png ├── install-in-edge.svg ├── install-in-github.png ├── install-in-github.svg ├── nut-mark-cover.png ├── nut-mark.svg ├── nut-unmark.svg └── screenshot.png ├── src ├── assets │ ├── icons │ │ ├── nut-mark-128x128.png │ │ ├── nut-mark-16x16.png │ │ ├── nut-mark-32x32.png │ │ ├── nut-mark-48x48.png │ │ ├── nut-unmark-128x128.png │ │ ├── nut-unmark-16x16.png │ │ ├── nut-unmark-32x32.png │ │ └── nut-unmark-48x48.png │ ├── introduction │ │ ├── pin-extension-cn.gif │ │ ├── pin-extension.gif │ │ ├── screenshot-cn.png │ │ └── screenshot.png │ ├── nut-mark.svg │ └── nut-unmark.svg ├── background │ └── main.js ├── composables │ ├── changeActionIcon.js │ ├── checkBookmarkState.js │ ├── getFaviconURL.js │ ├── getTabInfo.js │ ├── getTranslation.js │ └── getTreeId.js ├── popup │ ├── App.vue │ ├── components │ │ ├── BookmarkFolderSession.vue │ │ ├── BookmarkUrlSession.vue │ │ ├── FolderGrid.vue │ │ ├── FolderGridItem.vue │ │ └── SearchFolder.vue │ ├── index.html │ └── main.js └── style.css ├── tailwind.config.js └── vite.config.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | WebExtensions: true, 4 | browser: true, 5 | es2021: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'plugin:vue/vue3-recommended', 10 | 'airbnb-base', 11 | ], 12 | overrides: [ 13 | ], 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | sourceType: 'module', 17 | }, 18 | plugins: [ 19 | 'vue', 20 | ], 21 | rules: { 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: benbinbin 2 | custom: https://afdian.com/a/benbinbin -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | yarn.lock -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Benbinbin 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Nut Mark](./public/nut-mark-cover.png) 2 | 3 | # Nut Mark 4 | Nut Mark is a browser extension to help you collect bookmarks more easily. 5 | 6 | [![Install in Edge](./public/install-in-edge.svg)](https://microsoftedge.microsoft.com/addons/detail/ajoonjmbgcebacpkibmillgghjnoinbh) 7 | [![Install from source code](./public/install-in-github.svg)](https://github.com/Benbinbin/NutMark/releases) 8 | 9 | :bulb: check out the [Introduction Page](https://nutmark.benbinbin.com/) for more information. 10 | 11 | :clapper: check out the [Youtube Playlist](https://www.youtube.com/playlist?list=PLqLRbo_6ezAH4dX-RJxExHDVa956wKvTT) to see the DEMO. 12 | 13 | ## Why call "Nut Mark" 14 | I like to collect and organize bookmarks like a squirrel hoards nuts, so I build this browser extension and call it "Nut Mark". 15 | 16 | ## Feature 17 | 18 | Nut Mark focus on collecting bookmarks with the following features to make the process more smoothly and easily 19 | 20 | :sparkles: Clean "dirty" URL with one click 21 | 22 | :sparkles: Navigate freely through the directory tree 23 | 24 | :sparkles: Quick search to find the directory to hold the bookmark 25 | 26 | ## Screenshot 27 | 28 | ![Nut Mark Popup Page Screenshot](./public/screenshot.png) 29 | 30 | ## License 31 | [MIT](https://github.com/Benbinbin/NutMark/blob/main/LICENSE) 32 | 33 | ## Feedback 34 | If you have any problem or suggestion about this project, feel free to open an [issue](https://github.com/Benbinbin/NutMark/issues/new) in Github or contact with me by email [benthomsonbin@gmail.com](benthomsonbin@gmail.com) -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_description": { 3 | "message": "Nut Mark make bookmarking a breeze" 4 | }, 5 | "popup_header_show_similar_bookmarks_btn_title": { 6 | "message": "click the button to show the similar bookmarks list" 7 | }, 8 | "popup_header_bookmark_state_saved": { 9 | "message": "Saved" 10 | }, 11 | "popup_header_bookmark_state_unsaved": { 12 | "message": "Unsaved" 13 | }, 14 | "popup_header_delete_bookmarks_btn_content": { 15 | "message": "Delete" 16 | }, 17 | "popup_header_delete_bookmarks_btn_title": { 18 | "message": "click the button to delete bookmark" 19 | }, 20 | "popup_header_update_bookmarks_btn_content": { 21 | "message": "Update" 22 | }, 23 | "popup_header_update_bookmarks_btn_content_hover": { 24 | "message": "Save" 25 | }, 26 | "popup_header_update_bookmarks_btn_title": { 27 | "message": "click the button to update bookmark" 28 | }, 29 | "popup_header_create_bookmarks_btn_content": { 30 | "message": "Save" 31 | }, 32 | "popup_header_create_bookmarks_btn_title": { 33 | "message": "click the button to create a bookmark" 34 | }, 35 | "popup_header_info_btn_title": { 36 | "message": "click the button to learn more about Nut Mark" 37 | }, 38 | "popup_main_bookmark_title_section_name": { 39 | "message": "Name" 40 | }, 41 | "popup_main_bookmark_title_section_set_tab_name_btn_title": { 42 | "message": "click the button to set the tab name as bookmark name" 43 | }, 44 | "popup_main_bookmark_title_section_reset_btn_title": { 45 | "message": "click the button to reset bookmark name" 46 | }, 47 | "popup_main_bookmark_title_section_textarea_placeholder": { 48 | "message": "Please enter a name for the bookmark" 49 | }, 50 | "popup_modal_delete_bookmark_prompt_title": { 51 | "message": "Delete the Bookmark" 52 | }, 53 | "popup_modal_delete_bookmark_prompt_confirm_btn_title": { 54 | "message": "click the button to confirm" 55 | }, 56 | "popup_modal_delete_bookmark_prompt_confirm_btn_content": { 57 | "message": "Confirm" 58 | }, 59 | "popup_modal_delete_bookmark_prompt_cancel_btn_title": { 60 | "message": "click the button to cancel" 61 | }, 62 | "popup_modal_delete_bookmark_prompt_cancel_btn_content": { 63 | "message": "Cancel" 64 | }, 65 | "popup_modal_similar_bookmarks_list_title": { 66 | "message": "Similar Bookmarks" 67 | }, 68 | "popup_modal_similar_bookmarks_list_btn_title": { 69 | "message": "click the button to edit the bookmark information" 70 | }, 71 | "popup_main_bookmark_url_section_name": { 72 | "message": "URL" 73 | }, 74 | "popup_main_bookmark_url_section_format_warning": { 75 | "message": "Please Enter a Link in the Correct Format" 76 | }, 77 | "popup_main_bookmark_url_section_edit_bookmark_with_duplicated_url_btn_title": { 78 | "message": "click the button to edit the saved bookmark with the same url" 79 | }, 80 | "popup_main_bookmark_url_section_edit_bookmark_with_duplicated_url_btn_content": { 81 | "message": "A Saved bookmark with the same URL" 82 | }, 83 | "popup_main_bookmark_url_section_one_click_mode_btn_title": { 84 | "message": "click the button to enable the one click mode to edit URL" 85 | }, 86 | "popup_main_bookmark_url_section_reset_url_btn_title": { 87 | "message": "click the button to reset the URL" 88 | }, 89 | "popup_main_bookmark_url_section_textarea_placeholder": { 90 | "message": "Please Enter the Link of the Bookmark" 91 | }, 92 | "popup_main_bookmark_url_section_show_url_hostname_btn_title": { 93 | "message": "hover the button to highlight the hostname of the URL" 94 | }, 95 | "popup_main_bookmark_url_section_delete_url_path_btn_title": { 96 | "message": "click the button to delete the part of the URL from path to the end" 97 | }, 98 | "popup_main_bookmark_url_section_delete_url_search_btn_title": { 99 | "message": "click the button to delete the part of the URL from search to the end" 100 | }, 101 | "popup_main_bookmark_url_section_delete_url_hash_btn_title": { 102 | "message": "click the button to delete the part of the URL hash" 103 | }, 104 | "popup_main_bookmark_folder_section_name": { 105 | "message": "Folder" 106 | }, 107 | "popup_main_bookmark_folder_section_set_node_tree_id_btn_old_title": { 108 | "message": "click the button to show where the folder is located" 109 | }, 110 | "popup_main_bookmark_folder_section_set_node_tree_id_btn_prompt_content": { 111 | "message": "please select the folder 👇" 112 | }, 113 | "popup_main_bookmark_folder_section_set_node_tree_id_btn_new_title": { 114 | "message": "click the button to show where the new folder is located" 115 | }, 116 | "popup_main_bookmark_folder_section_toggle_search_folder_btn_title": { 117 | "message": "click the button to toggle showing or hiding the search folder panel" 118 | }, 119 | "popup_main_bookmark_folder_section_reset_folder_btn_title": { 120 | "message": "click the button to reset the folder of bookmark" 121 | }, 122 | "popup_main_bookmark_folder_section_toggle_bookmark_btn_title": { 123 | "message": "show bookmark or just show folder" 124 | }, 125 | "popup_main_bookmark_folder_section_folder_path_nav_btn_title": { 126 | "message": "click the button to show where the folder is located" 127 | }, 128 | "popup_main_bookmark_folder_section_folder_path_nav_root_btn_content": { 129 | "message": "Root Folder" 130 | }, 131 | "popup_main_bookmark_folder_section_add_new_folder_btn_title": { 132 | "message": "click the button to add new folder" 133 | }, 134 | "popup_main_bookmark_folder_section_new_folder_name_input_placeholder": { 135 | "message": "Please Enter a Directory Name" 136 | }, 137 | "popup_main_bookmark_folder_section_confirm_new_folder_name_btn_title": { 138 | "message": "click the button to confirm the new folder name" 139 | }, 140 | "popup_main_bookmark_folder_section_edit_new_folder_name_btn_title": { 141 | "message": "click the button to edit the new folder name" 142 | }, 143 | "popup_main_bookmark_folder_section_open_folder_btn_title": { 144 | "message": "click the button to open the folder in place" 145 | }, 146 | "popup_main_bookmark_folder_section_expand_folder_btn_title": { 147 | "message": "click the button to expand the folder" 148 | }, 149 | "popup_main_bookmark_folder_section_close_folder_btn_title": { 150 | "message": "click the button to close the folder" 151 | }, 152 | "popup_main_bookmark_folder_section_select_folder_btn_title": { 153 | "message": "click to select this folder" 154 | }, 155 | "popup_main_bookmark_folder_section_folder_nav_btn_title": { 156 | "message": "click to show this folder" 157 | }, 158 | "popup_main_bookmark_folder_section_folder_nav_left_scroll_btn_title": { 159 | "message": "click the button to scroll to the left" 160 | }, 161 | "popup_main_bookmark_folder_section_folder_nav_right_scroll_btn_title": { 162 | "message": "click the button to scroll to the right" 163 | }, 164 | "popup_main_bookmark_folder_section_search_folder_include_bookmark_btn_title": { 165 | "message": "click the button to toggle search entry include bookmarks or note" 166 | }, 167 | "popup_main_bookmark_folder_section_search_folder_input_placeholder": { 168 | "message": "Please Enter Content to Search" 169 | }, 170 | "popup_main_bookmark_folder_section_search_folder_focus_to_input_box_btn_title": { 171 | "message": "click to focus the input box" 172 | }, 173 | "popup_main_bookmark_folder_section_search_folder_state_waiting": { 174 | "message": "Type to Search" 175 | }, 176 | "popup_main_bookmark_folder_section_search_folder_state_search": { 177 | "message": "Searching" 178 | }, 179 | "popup_main_bookmark_folder_section_search_folder_state_solved": { 180 | "message": "Oops! Empty Entry" 181 | }, 182 | "bookmark_folder_default_name": { 183 | "message": "Unnamed Folder" 184 | }, 185 | "bookmark_default_name": { 186 | "message": "Unnamed Bookmark" 187 | }, 188 | "bookmark_favicon_img_alt": { 189 | "message": "bookmark icon" 190 | }, 191 | "bookmark_new_folder_default_title": { 192 | "message": "New folder" 193 | } 194 | } -------------------------------------------------------------------------------- /_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extension_description": { 3 | "message": "Nut Mark 让收藏书签变得更轻松" 4 | }, 5 | "popup_header_show_similar_bookmarks_btn_title": { 6 | "message": "点击按钮显示相似书签列表" 7 | }, 8 | "popup_header_bookmark_state_saved": { 9 | "message": "已保存" 10 | }, 11 | "popup_header_bookmark_state_unsaved": { 12 | "message": "未保存" 13 | }, 14 | "popup_header_delete_bookmarks_btn_content": { 15 | "message": "删除" 16 | }, 17 | "popup_header_delete_bookmarks_btn_title": { 18 | "message": "点击按钮删除书签" 19 | }, 20 | "popup_header_update_bookmarks_btn_content": { 21 | "message": "更新" 22 | }, 23 | "popup_header_update_bookmarks_btn_content_hover": { 24 | "message": "保存" 25 | }, 26 | "popup_header_update_bookmarks_btn_title": { 27 | "message": "点击按钮更新书签" 28 | }, 29 | "popup_header_create_bookmarks_btn_content": { 30 | "message": "保存" 31 | }, 32 | "popup_header_create_bookmarks_btn_title": { 33 | "message": "点击按钮创建书签" 34 | }, 35 | "popup_header_info_btn_title": { 36 | "message": "点击按钮以了解更多 Nut Mark" 37 | }, 38 | "popup_main_bookmark_title_section_name": { 39 | "message": "名称" 40 | }, 41 | "popup_main_bookmark_title_section_set_tab_name_btn_title": { 42 | "message": "点击按钮将选项卡名称设置为书签名称" 43 | }, 44 | "popup_main_bookmark_title_section_reset_btn_title": { 45 | "message": "点击按钮重置书签名称" 46 | }, 47 | "popup_main_bookmark_title_section_textarea_placeholder": { 48 | "message": "请输入书签名称" 49 | }, 50 | "popup_modal_delete_bookmark_prompt_title": { 51 | "message": "删除书签" 52 | }, 53 | "popup_modal_delete_bookmark_prompt_confirm_btn_title": { 54 | "message": "点击按钮确认" 55 | }, 56 | "popup_modal_delete_bookmark_prompt_confirm_btn_content": { 57 | "message": "确认" 58 | }, 59 | "popup_modal_delete_bookmark_prompt_cancel_btn_title": { 60 | "message": "点击按钮取消" 61 | }, 62 | "popup_modal_delete_bookmark_prompt_cancel_btn_content": { 63 | "message": "取消" 64 | }, 65 | "popup_modal_similar_bookmarks_list_title": { 66 | "message": "相似书签" 67 | }, 68 | "popup_modal_similar_bookmarks_list_btn_title": { 69 | "message": "点击按钮编辑书签信息" 70 | }, 71 | "popup_main_bookmark_url_section_name": { 72 | "message": "网址" 73 | }, 74 | "popup_main_bookmark_url_section_format_warning": { 75 | "message": "请输入正确格式的链接" 76 | }, 77 | "popup_main_bookmark_url_section_edit_bookmark_with_duplicated_url_btn_title": { 78 | "message": "点击按钮编辑已保存的相同网址的书签" 79 | }, 80 | "popup_main_bookmark_url_section_edit_bookmark_with_duplicated_url_btn_content": { 81 | "message": "已保存相同网址的书签" 82 | }, 83 | "popup_main_bookmark_url_section_one_click_mode_btn_title": { 84 | "message": "点击按钮启用一键编辑网址模式" 85 | }, 86 | "popup_main_bookmark_url_section_reset_url_btn_title": { 87 | "message": "点击按钮重置网址" 88 | }, 89 | "popup_main_bookmark_url_section_textarea_placeholder": { 90 | "message": "请输入书签的链接" 91 | }, 92 | "popup_main_bookmark_url_section_show_url_hostname_btn_title": { 93 | "message": "悬停在按钮上以突出显示链接的主机名" 94 | }, 95 | "popup_main_bookmark_url_section_delete_url_path_btn_title": { 96 | "message": "点击按钮删除链接路径到结尾的部分" 97 | }, 98 | "popup_main_bookmark_url_section_delete_url_search_btn_title": { 99 | "message": "点击按钮删除链接搜索到结尾的部分" 100 | }, 101 | "popup_main_bookmark_url_section_delete_url_hash_btn_title": { 102 | "message": "点击按钮删除链接哈希部分" 103 | }, 104 | "popup_main_bookmark_folder_section_name": { 105 | "message": "文件夹" 106 | }, 107 | "popup_main_bookmark_folder_section_set_node_tree_id_btn_old_title": { 108 | "message": "点击按钮显示文件夹所在位置" 109 | }, 110 | "popup_main_bookmark_folder_section_set_node_tree_id_btn_prompt_content": { 111 | "message": "请选择文件夹 👇" 112 | }, 113 | "popup_main_bookmark_folder_section_set_node_tree_id_btn_new_title": { 114 | "message": "点击按钮显示新文件夹所在位置" 115 | }, 116 | "popup_main_bookmark_folder_section_toggle_search_folder_btn_title": { 117 | "message": "点击按钮切换显示或隐藏搜索文件夹面板" 118 | }, 119 | "popup_main_bookmark_folder_section_reset_folder_btn_title": { 120 | "message": "点击按钮重置书签的文件夹" 121 | }, 122 | "popup_main_bookmark_folder_section_toggle_bookmark_btn_title": { 123 | "message": "显示书签或只显示文件夹" 124 | }, 125 | "popup_main_bookmark_folder_section_folder_path_nav_btn_title": { 126 | "message": "点击按钮显示文件夹所在位置" 127 | }, 128 | "popup_main_bookmark_folder_section_folder_path_nav_root_btn_content": { 129 | "message": "根目录" 130 | }, 131 | "popup_main_bookmark_folder_section_add_new_folder_btn_title": { 132 | "message": "点击按钮添加新的文件夹" 133 | }, 134 | "popup_main_bookmark_folder_section_new_folder_name_input_placeholder": { 135 | "message": "请输入目录名称" 136 | }, 137 | "popup_main_bookmark_folder_section_confirm_new_folder_name_btn_title": { 138 | "message": "点击按钮确认新文件夹名称" 139 | }, 140 | "popup_main_bookmark_folder_section_edit_new_folder_name_btn_title": { 141 | "message": "点击按钮编辑新文件夹名称" 142 | }, 143 | "popup_main_bookmark_folder_section_open_folder_btn_title": { 144 | "message": "点击按钮在原地打开文件夹" 145 | }, 146 | "popup_main_bookmark_folder_section_expand_folder_btn_title": { 147 | "message": "点击按钮展开文件夹" 148 | }, 149 | "popup_main_bookmark_folder_section_close_folder_btn_title": { 150 | "message": "点击按钮关闭文件夹" 151 | }, 152 | "popup_main_bookmark_folder_section_select_folder_btn_title": { 153 | "message": "点击选择此文件夹" 154 | }, 155 | "popup_main_bookmark_folder_section_folder_nav_btn_title": { 156 | "message": "单击以显示此文件夹" 157 | }, 158 | "popup_main_bookmark_folder_section_folder_nav_left_scroll_btn_title": { 159 | "message": "点击按钮向左滚动" 160 | }, 161 | "popup_main_bookmark_folder_section_folder_nav_right_scroll_btn_title": { 162 | "message": "点击按钮向右滚动" 163 | }, 164 | "popup_main_bookmark_folder_section_search_folder_include_bookmark_btn_title": { 165 | "message": "点击按钮切换搜索条目包括书签或注释" 166 | }, 167 | "popup_main_bookmark_folder_section_search_folder_input_placeholder": { 168 | "message": "请输入要搜索的内容" 169 | }, 170 | "popup_main_bookmark_folder_section_search_folder_focus_to_input_box_btn_title": { 171 | "message": "单击聚焦输入框" 172 | }, 173 | "popup_main_bookmark_folder_section_search_folder_state_waiting": { 174 | "message": "输入搜索" 175 | }, 176 | "popup_main_bookmark_folder_section_search_folder_state_search": { 177 | "message": "搜索中" 178 | }, 179 | "popup_main_bookmark_folder_section_search_folder_state_solved": { 180 | "message": "抱歉!未搜索到条目" 181 | }, 182 | "bookmark_folder_default_name": { 183 | "message": "未命名文件夹" 184 | }, 185 | "bookmark_default_name": { 186 | "message": "未命名书签" 187 | }, 188 | "bookmark_favicon_img_alt": { 189 | "message": "书签图标" 190 | }, 191 | "bookmark_new_folder_default_title": { 192 | "message": "新建文件夹" 193 | } 194 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Nut Mark", 4 | "version": "1.1.0", 5 | "description": "__MSG_extension_description__", 6 | "permissions": [ 7 | "bookmarks", 8 | "tabs", 9 | "favicon", 10 | "storage" 11 | ], 12 | "default_locale": "en", 13 | "background": { 14 | "service_worker": "src/background/main.js", 15 | "type": "module" 16 | }, 17 | "action": { 18 | "default_title": "Nut Mark", 19 | "default_popup": "src/popup/index.html", 20 | "default_icon": { 21 | "16": "src/assets/icons/nut-unmark-16x16.png", 22 | "32": "src/assets/icons/nut-unmark-32x32.png", 23 | "48": "src/assets/icons/nut-unmark-48x48.png", 24 | "128": "src/assets/icons/nut-unmark-128x128.png" 25 | } 26 | }, 27 | "icons": { 28 | "16": "src/assets/icons/nut-mark-16x16.png", 29 | "32": "src/assets/icons/nut-mark-32x32.png", 30 | "48": "src/assets/icons/nut-mark-48x48.png", 31 | "128": "src/assets/icons/nut-mark-128x128.png" 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nut-mark", 3 | "private": true, 4 | "version": "1.1.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.2.47" 13 | }, 14 | "devDependencies": { 15 | "@crxjs/vite-plugin": "^2.0.0-beta.16", 16 | "@iconify/vue": "^4.1.1", 17 | "@vitejs/plugin-vue": "^4.2.1", 18 | "autoprefixer": "^10.4.14", 19 | "eslint": "^7.32.0 || ^8.2.0", 20 | "eslint-config-airbnb-base": "^15.0.0", 21 | "eslint-plugin-import": "^2.25.2", 22 | "eslint-plugin-vue": "^9.10.0", 23 | "postcss": "^8.4.22", 24 | "sass": "^1.62.0", 25 | "tailwindcss": "^3.3.1", 26 | "vite": "^4.3.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/icons/nut-mark-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-mark-128x128.png -------------------------------------------------------------------------------- /public/icons/nut-mark-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-mark-16x16.png -------------------------------------------------------------------------------- /public/icons/nut-mark-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-mark-32x32.png -------------------------------------------------------------------------------- /public/icons/nut-mark-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-mark-48x48.png -------------------------------------------------------------------------------- /public/icons/nut-unmark-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-unmark-128x128.png -------------------------------------------------------------------------------- /public/icons/nut-unmark-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-unmark-16x16.png -------------------------------------------------------------------------------- /public/icons/nut-unmark-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-unmark-32x32.png -------------------------------------------------------------------------------- /public/icons/nut-unmark-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/icons/nut-unmark-48x48.png -------------------------------------------------------------------------------- /public/install-in-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/install-in-chrome.png -------------------------------------------------------------------------------- /public/install-in-chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /public/install-in-edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/install-in-edge.png -------------------------------------------------------------------------------- /public/install-in-edge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/install-in-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/install-in-github.png -------------------------------------------------------------------------------- /public/install-in-github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/nut-mark-cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/nut-mark-cover.png -------------------------------------------------------------------------------- /public/nut-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/nut-unmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/public/screenshot.png -------------------------------------------------------------------------------- /src/assets/icons/nut-mark-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-mark-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/nut-mark-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-mark-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/nut-mark-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-mark-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/nut-mark-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-mark-48x48.png -------------------------------------------------------------------------------- /src/assets/icons/nut-unmark-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-unmark-128x128.png -------------------------------------------------------------------------------- /src/assets/icons/nut-unmark-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-unmark-16x16.png -------------------------------------------------------------------------------- /src/assets/icons/nut-unmark-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-unmark-32x32.png -------------------------------------------------------------------------------- /src/assets/icons/nut-unmark-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/icons/nut-unmark-48x48.png -------------------------------------------------------------------------------- /src/assets/introduction/pin-extension-cn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/introduction/pin-extension-cn.gif -------------------------------------------------------------------------------- /src/assets/introduction/pin-extension.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/introduction/pin-extension.gif -------------------------------------------------------------------------------- /src/assets/introduction/screenshot-cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/introduction/screenshot-cn.png -------------------------------------------------------------------------------- /src/assets/introduction/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Benbinbin/NutMark/7f8a9cf2e88b5520b59b25340eb7fe50e2751643/src/assets/introduction/screenshot.png -------------------------------------------------------------------------------- /src/assets/nut-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/nut-unmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/background/main.js: -------------------------------------------------------------------------------- 1 | import { useCheckBookmarkState } from '@/composables/checkBookmarkState' 2 | import { useChangeActionIcon } from '@/composables/changeActionIcon' 3 | 4 | 5 | /** 6 | * show page after install extension 7 | */ 8 | // open the introduction page when the extension is installed 9 | chrome.runtime.onInstalled.addListener(({reason}) => { 10 | if (reason === 'install') { 11 | chrome.tabs.create({ 12 | url: 'https://nutmark.benbinbin.com' 13 | }) 14 | } 15 | }); 16 | 17 | /** 18 | * change action icon 19 | */ 20 | const checkActiveWindowTabState = async () => { 21 | // get the active tab of current focus window 22 | const [tab] = await chrome.tabs.query({ 23 | active: true, 24 | currentWindow: true, 25 | }); 26 | 27 | if (!tab) return; 28 | 29 | const url = tab.url || tab.pendingUrl; 30 | 31 | let bookmarkState = false; 32 | if (url) { 33 | const result = await useCheckBookmarkState(url); 34 | if(result) bookmarkState = true 35 | } 36 | 37 | // change action icon 38 | await useChangeActionIcon(bookmarkState); 39 | } 40 | 41 | // watching the active tab change 42 | chrome.tabs.onActivated.addListener(async (activeInfo) => { 43 | await checkActiveWindowTabState(); 44 | }); 45 | 46 | // watching the window focus change 47 | chrome.windows.onFocusChanged.addListener(async (windowId) => { 48 | await checkActiveWindowTabState(); 49 | }); 50 | 51 | // watching tab (url) update 52 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 53 | if (!changeInfo.url) return; 54 | 55 | await checkActiveWindowTabState(); 56 | }); 57 | 58 | // watching bookmark create 59 | chrome.bookmarks.onCreated.addListener(async (id, bookmarkNode) => { 60 | await checkActiveWindowTabState(); 61 | }); 62 | 63 | // watching bookmark delete 64 | chrome.bookmarks.onRemoved.addListener(async (id, bookmarkNode) => { 65 | await checkActiveWindowTabState(); 66 | }); 67 | -------------------------------------------------------------------------------- /src/composables/changeActionIcon.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * change action icon 4 | */ 5 | export async function useChangeActionIcon(state = false) { 6 | let iconsPath = { 7 | "16": "src/assets/icons/nut-unmark-16x16.png", 8 | "32": "src/assets/icons/nut-unmark-32x32.png", 9 | "48": "src/assets/icons/nut-unmark-48x48.png", 10 | "128": "src/assets/icons/nut-unmark-128x128.png" 11 | } 12 | 13 | if (state) { 14 | iconsPath = { 15 | "16": "src/assets/icons/nut-mark-16x16.png", 16 | "32": "src/assets/icons/nut-mark-32x32.png", 17 | "48": "src/assets/icons/nut-mark-48x48.png", 18 | "128": "src/assets/icons/nut-mark-128x128.png" 19 | } 20 | } 21 | 22 | await chrome.action.setIcon({ 23 | path: iconsPath 24 | }); 25 | }; -------------------------------------------------------------------------------- /src/composables/checkBookmarkState.js: -------------------------------------------------------------------------------- 1 | export async function useCheckBookmarkState(url) { 2 | const nodes = await chrome.bookmarks.search({ 3 | url, 4 | }); 5 | if (nodes.length === 0) { 6 | return false; 7 | } 8 | const bookmark = nodes[0]; 9 | return bookmark; 10 | }; -------------------------------------------------------------------------------- /src/composables/getFaviconURL.js: -------------------------------------------------------------------------------- 1 | // refer to https://developer.chrome.com/docs/extensions/mv3/favicon/ 2 | export function useGetFaviconURL(link) { 3 | const url = new URL(chrome.runtime.getURL('/_favicon/')); 4 | url.searchParams.set("pageUrl", link); 5 | url.searchParams.set("size", "32"); 6 | return url.toString(); 7 | } 8 | -------------------------------------------------------------------------------- /src/composables/getTabInfo.js: -------------------------------------------------------------------------------- 1 | export async function useGetTabInfo() { 2 | // get current tab 3 | const [tab] = await chrome.tabs.query({ 4 | active: true, 5 | currentWindow: true, 6 | }); 7 | 8 | return { 9 | tabTitle: tab.title, 10 | tabUrl: tab.url || tab.pendingUrl 11 | } 12 | } -------------------------------------------------------------------------------- /src/composables/getTranslation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * i18n 3 | */ 4 | export function useGetTranslation(title) { 5 | return chrome.i18n.getMessage(title) 6 | } -------------------------------------------------------------------------------- /src/composables/getTreeId.js: -------------------------------------------------------------------------------- 1 | /** 2 | * get the folder id for the node tree based on a given folder id 3 | */ 4 | export async function useGetTreeId(folderId) { 5 | const resultForFolderNode = await chrome.bookmarks.get(folderId); 6 | const folderNode = resultForFolderNode[0]; 7 | 8 | let targetId; 9 | if (folderNode.parentId) { 10 | // if the folder node has parent folder 11 | targetId = folderNode.parentId; 12 | } else { 13 | // if the folder node doesn't have parent folder 14 | // (when the folder is the root folder) 15 | // then set the node tree id as this folder 16 | targetId = folderNode.id; 17 | } 18 | return targetId; 19 | } -------------------------------------------------------------------------------- /src/popup/App.vue: -------------------------------------------------------------------------------- 1 | 253 | 254 | 376 | 377 | 430 | -------------------------------------------------------------------------------- /src/popup/components/BookmarkFolderSession.vue: -------------------------------------------------------------------------------- 1 | 113 | 114 | 153 | 154 | -------------------------------------------------------------------------------- /src/popup/components/BookmarkUrlSession.vue: -------------------------------------------------------------------------------- 1 | 169 | 170 | 268 | 269 | -------------------------------------------------------------------------------- /src/popup/components/FolderGrid.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 127 | 128 | -------------------------------------------------------------------------------- /src/popup/components/FolderGridItem.vue: -------------------------------------------------------------------------------- 1 | 222 | 342 | 343 | 367 | -------------------------------------------------------------------------------- /src/popup/components/SearchFolder.vue: -------------------------------------------------------------------------------- 1 | 131 | 132 | 190 | 191 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Popup - Nuxt Mark 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/popup/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | // eslint-disable-next-line import/no-unresolved 3 | import '@/style.css'; 4 | import { useGetTabInfo } from '@/composables/getTabInfo'; 5 | import { useCheckBookmarkState } from '@/composables/checkBookmarkState'; 6 | import { useGetTreeId } from '@/composables/getTreeId' 7 | import App from './App.vue'; 8 | 9 | // the tab info 10 | let tabTitle = ''; 11 | let tabUrl = ''; 12 | 13 | // the bookmark info (if the tab has bookmarked) 14 | // let bookmarkState = false; 15 | let bookmarkId = null; 16 | let bookmarkTitle = ''; 17 | let bookmarkUrl = ''; 18 | // init value is "1" 19 | // this id stand for the "Bookmarks bar"/「书签栏」 directory 20 | let bookmarkFolderId = '1'; 21 | // init value is "0" 22 | // this id stand for the root folder 23 | let nodeTreeId = '0'; 24 | 25 | // the similar bookmarks 26 | let similarBookmarks = []; 27 | 28 | // get information from tab or recent created bookmark 29 | const setBookmarkParameters = async () => { 30 | const resultForTab = await useGetTabInfo(); 31 | tabTitle = resultForTab.tabTitle; 32 | tabUrl = resultForTab.tabUrl; 33 | 34 | // check the tab url bookmark state 35 | const resultForBookmark = await useCheckBookmarkState(tabUrl); 36 | 37 | if (resultForBookmark) { 38 | // if the tab has already bookmarked 39 | bookmarkId = resultForBookmark.id; 40 | bookmarkTitle = resultForBookmark.title; 41 | bookmarkUrl = resultForBookmark.url; 42 | bookmarkFolderId = resultForBookmark.parentId; 43 | } else { 44 | // if the tab doesn't bookmark 45 | // get recent bookmark to set folder 46 | const recentBookmarkArr = await chrome.bookmarks.getRecent(1); 47 | 48 | if (recentBookmarkArr.length > 0) { 49 | // if there is a bookmark created recently 50 | const recentBookmark = recentBookmarkArr[0]; 51 | bookmarkFolderId = recentBookmark.parentId; 52 | } else { 53 | // if there isn't any bookmarks created recently 54 | // (when this is a brand new browser) 55 | // set the selected folder as the first children node 56 | // 根目录下的第一个子文件夹一般就是「书签栏」 57 | bookmarkFolderId = '1'; 58 | // and the node tree will be start at the root node 59 | } 60 | } 61 | 62 | nodeTreeId = await useGetTreeId(bookmarkFolderId) 63 | 64 | // get the similar bookmarks 65 | const targetUrl = bookmarkUrl || tabUrl; 66 | if(targetUrl) { 67 | try { 68 | const urlObj = new URL(targetUrl); 69 | const urlHostname = urlObj.hostname; 70 | if(urlHostname) { 71 | const nodesArr = await chrome.bookmarks.search(urlHostname) 72 | similarBookmarks = nodesArr.filter(node => { 73 | // only get the bookmark node (with url) 74 | // and not the current bookmark (if the tab has bookmarked already) 75 | return node.url && node.id !== bookmarkId; 76 | }) 77 | similarBookmarks.sort((a, b) => { 78 | if(a.url.length < b.url.length) { 79 | return -1 80 | } else if(a.url.length > b.url.length) { 81 | return 1 82 | } else { 83 | return 0 84 | } 85 | }) 86 | } 87 | } catch (error) { 88 | console.log(error); 89 | } 90 | } 91 | } 92 | 93 | setBookmarkParameters().then(() => { 94 | createApp(App, { 95 | tabTitle, 96 | tabUrl, 97 | bookmarkId, 98 | bookmarkTitle, 99 | bookmarkUrl, 100 | bookmarkFolderId, 101 | nodeTreeId, 102 | similarBookmarks 103 | }).mount('#app'); 104 | }) 105 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body::-webkit-scrollbar { 6 | width: 8px; 7 | } 8 | 9 | body::-webkit-scrollbar-thumb { 10 | border-radius: 4px; 11 | background-color: #d1d5db; 12 | } 13 | 14 | body::-webkit-scrollbar-thumb:hover { 15 | background-color: #9ca3af; 16 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/**/*.html', 5 | './src/**/*.{js,vue}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { crx } from '@crxjs/vite-plugin' 5 | // import manifest from './manifest.json' // Node 14 & 16 6 | import manifest from './manifest.json' assert { type: 'json' } // Node >=17 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | resolve: { 11 | alias: { 12 | '@': resolve(__dirname, 'src'), 13 | }, 14 | }, 15 | plugins: [ 16 | vue(), 17 | crx({ manifest }), 18 | ], 19 | server: { 20 | port: 3000, 21 | hmr: { 22 | port: 3000 23 | } 24 | } 25 | }) 26 | --------------------------------------------------------------------------------