├── .gitignore ├── _locales ├── en │ └── messages.json ├── ja │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── background.js ├── css ├── options.css ├── popup.css ├── shadow-viewer.css └── support.css ├── eslint.config.mjs ├── icon ├── GitHub-Mark.png ├── icon.png ├── icon128.png ├── icon16.png └── ko-fi.png ├── image-viewer.js ├── license ├── manifest.json ├── package-lock.json ├── package.json ├── page ├── options.html ├── options.js ├── popup.html ├── popup.js ├── support.html └── support.js ├── readme.md ├── scripts ├── action-canvas.js ├── action-folder.js ├── action-image.js ├── action-page.js ├── activate-url.js ├── activate-worker.js ├── download-images.js ├── extract-iframe.js ├── hook.js └── utility.js └── version.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules/ 3 | web-ext-artifacts -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_new_custom": { 3 | "message": "add new custom" 4 | }, 5 | "app_desc": { 6 | "message": "More than the default image viewer" 7 | }, 8 | "app_name": { 9 | "message": "Image Viewer" 10 | }, 11 | "auto_desc": { 12 | "message": "When you use auto navigation, Image Viewer will navigate to the next image approximately this period of time.
When this period is set close to 0, navigation speed will only depend on your computer's performance." 13 | }, 14 | "auto_period": { 15 | "message": "Auto navigate period" 16 | }, 17 | "auto_scroll_desc": { 18 | "message": "Some web pages load more content as you scroll down.
Add domains here to enable active auto scroll for those websites." 19 | }, 20 | "auto_scroll_enable_list": { 21 | "message": "Enable Auto Scroll" 22 | }, 23 | "custom_search": { 24 | "message": "Custom" 25 | }, 26 | "custom_search_url": { 27 | "message": "Custom URL" 28 | }, 29 | "debounce_desc": { 30 | "message": "When you 'long press arrow key', Image Viewer will stay at the first/last image before goto other end.
A timer for this period will start counting and continue your action when it stops and is not interrupted.
Set debounce period to 0 if you don't want this behavior." 31 | }, 32 | "debounce_period": { 33 | "message": "Debounce period" 34 | }, 35 | "default_image_fit": { 36 | "message": "Default image fit" 37 | }, 38 | "domain_settings_example": { 39 | "message": "Example setting (support regex):
example.com
sub.example.com
another.com
/^https.*regex\\.com/" 40 | }, 41 | "domain_specific": { 42 | "message": "Domain specific settings" 43 | }, 44 | "download_hotkey": { 45 | "message": "Download collected images" 46 | }, 47 | "fit_with_original_size": { 48 | "message": "Original size" 49 | }, 50 | "fit_with_original_size_not_exceeds_window": { 51 | "message": "Original size (not exceeds window)" 52 | }, 53 | "fit_with_window": { 54 | "message": "Fit with window" 55 | }, 56 | "fit_with_window_height": { 57 | "message": "Fit with window height" 58 | }, 59 | "fit_with_window_width": { 60 | "message": "Fit with window width" 61 | }, 62 | "function_hotkey": { 63 | "message": "Function hotkey" 64 | }, 65 | "github_link": { 66 | "message": "Github user manual" 67 | }, 68 | "height": { 69 | "message": "Height" 70 | }, 71 | "hotkey_notice": { 72 | "message": "When more than one search using same hotkey, all search will be triggered, built-in hotkey like 'Ctrl + W' or 'Ctrl + T' will not be overridden.

When using custom search, '{imgSrc}' will be used to replace with real URL

Soft disable hotkey without Ctrl/Alt/Shift key, change its value in DevTools to save it." 73 | }, 74 | "hover_check_desc": { 75 | "message": "Image Viewer allows you to select images by right-clicking on them.
By default, it removes hover elements to select video thumbnails.
Add domains here if you want to keep the hover elements for those websites." 76 | }, 77 | "hover_disable_list": { 78 | "message": "Disable Hover Check" 79 | }, 80 | "image_size": { 81 | "message": "Size" 82 | }, 83 | "image_source": { 84 | "message": "Source" 85 | }, 86 | "image_type": { 87 | "message": "Type" 88 | }, 89 | "image_unlazy_desc": { 90 | "message": "Some web pages serve extremely large images through lazy loading (eg. r/EarthPorn).
Add domains here if you want to disable image unlazy for those websites." 91 | }, 92 | "image_unlazy_disable_list": { 93 | "message": "Disable Image Unlazy" 94 | }, 95 | "kofi_link": { 96 | "message": "Support me" 97 | }, 98 | "min_height": { 99 | "message": "Min. height" 100 | }, 101 | "min_width": { 102 | "message": "Min. width" 103 | }, 104 | "new_option": { 105 | "message": "New option is available" 106 | }, 107 | "not_factor_of_360": { 108 | "message": "Not a factor of 360" 109 | }, 110 | "options": { 111 | "message": "Options" 112 | }, 113 | "options_title": { 114 | "message": "Options - Image Viewer" 115 | }, 116 | "popup_title": { 117 | "message": "Release Notes - Image Viewer" 118 | }, 119 | "release_notes": { 120 | "message": "Release notes" 121 | }, 122 | "reset": { 123 | "message": "Reset" 124 | }, 125 | "rotate_degree": { 126 | "message": "Rotate degree" 127 | }, 128 | "save": { 129 | "message": "Save" 130 | }, 131 | "scroll_hotkey": { 132 | "message": "Enable active auto scroll" 133 | }, 134 | "search_hotkey": { 135 | "message": "Image search hotkey" 136 | }, 137 | "size_filter": { 138 | "message": "Image size filter" 139 | }, 140 | "speed_control": { 141 | "message": "Speed control" 142 | }, 143 | "support": { 144 | "message": "Support" 145 | }, 146 | "support_title": { 147 | "message": "Support - Image Viewer" 148 | }, 149 | "svg_filter": { 150 | "message": "Filter svg" 151 | }, 152 | "throttle_desc": { 153 | "message": "When you 'long press arrow key', Image Viewer will display each image approximate this period of time.
The default setting 80ms = 12.5fps (12.5 image per 1 second).
When throttle period was set to be close to 0, fps limit will only depending on you computer performance." 154 | }, 155 | "throttle_period": { 156 | "message": "Throttle period" 157 | }, 158 | "use_all_search": { 159 | "message": "All of above" 160 | }, 161 | "view_all_images_in_image_viewer": { 162 | "message": "View images (no filter)" 163 | }, 164 | "view_canvas_in_image_viewer": { 165 | "message": "View canvases in Image Viewer" 166 | }, 167 | "view_images_in_image_viewer": { 168 | "message": "View images in Image Viewer" 169 | }, 170 | "view_last_right_click_image_in_image_viewer": { 171 | "message": "View images (last right click)" 172 | }, 173 | "width": { 174 | "message": "Width" 175 | }, 176 | "zoom_ratio": { 177 | "message": "Zoom ratio" 178 | } 179 | } -------------------------------------------------------------------------------- /_locales/ja/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_new_custom": { 3 | "message": "カスタムを追加" 4 | }, 5 | "app_desc": { 6 | "message": "画像観覧をもっと使いやすいように機能を強化します" 7 | }, 8 | "app_name": { 9 | "message": "Image Viewer" 10 | }, 11 | "auto_desc": { 12 | "message": "自動観覧を使用すると、Image Viewerはこの時間間隔で次の画像に移動します。
自動観覧間隔が 0 に近い値に設定されている場合、実際の表示速度はコンピューターの性能に依存します" 13 | }, 14 | "auto_period": { 15 | "message": "自動観覧間隔" 16 | }, 17 | "auto_scroll_desc": { 18 | "message": "一部のWebページでは、スクロールダウンするとコンテンツが追加で読み込まれます
Image Viewerのアクティブ自動スクロール機能を有効にしたい場合は、ここにドメインを追加してください" 19 | }, 20 | "auto_scroll_enable_list": { 21 | "message": "自動スクロールを有効にする" 22 | }, 23 | "custom_search": { 24 | "message": "カスタム" 25 | }, 26 | "custom_search_url": { 27 | "message": "カスタム URL" 28 | }, 29 | "debounce_desc": { 30 | "message": "「矢印キーを長押し」すると、Image Viewerは最初/最後の画像にとどまり
遅延が終了し、他の操作によって中断されない場合、前の操作が続行されます
この動作を停止するには、遅延時間を 0 に設定してください" 31 | }, 32 | "debounce_period": { 33 | "message": "デバウンスタイム" 34 | }, 35 | "default_image_fit": { 36 | "message": "デフォルトの画像サイズ" 37 | }, 38 | "domain_settings_example": { 39 | "message": "設定例(正規表現対応):
example.com
sub.example.com
another.com
/^https.*regex\\.com/" 40 | }, 41 | "domain_specific": { 42 | "message": "ドメイン固有の設定" 43 | }, 44 | "download_hotkey": { 45 | "message": "集めた写真をダウンロード" 46 | }, 47 | "fit_with_original_size": { 48 | "message": "画像の元のサイズ" 49 | }, 50 | "fit_with_original_size_not_exceeds_window": { 51 | "message": "画像の元のサイズ(ウィンドウを超えない)" 52 | }, 53 | "fit_with_window": { 54 | "message": "ウィンドウのサイズ" 55 | }, 56 | "fit_with_window_height": { 57 | "message": "ウィンドウの高度" 58 | }, 59 | "fit_with_window_width": { 60 | "message": "ウィンドウの横幅" 61 | }, 62 | "function_hotkey": { 63 | "message": "機能ホットキー" 64 | }, 65 | "github_link": { 66 | "message": "Github ユーザマニュアル(Eng)" 67 | }, 68 | "height": { 69 | "message": "高度" 70 | }, 71 | "hotkey_notice": { 72 | "message": "複数の検索で同じホットキーを使用すると、すべての検索が同時にトリガーされ、「Ctrl + W」「Ctrl + T」などの既存のショートカットキーはオーバーライドされません

カスタム検索エンジンを使用する場合、検索URLの'{imgSrc}'は実際の画像リンクに置き換えられ

Ctrl/Alt/Shiftを使用せずのホットキーは推奨しません、たがDevToolsで望みのキーに変更そして保存はできます" 73 | }, 74 | "hover_check_desc": { 75 | "message": "Image Viewerは、右クリックで画像を選択できます
ビデオのサムネイルを選択するには、デフォルトでホバー要素を削除します
ホバー要素を保持したい場合は、ここにドメインを追加してください" 76 | }, 77 | "hover_disable_list": { 78 | "message": "ホバーチェックを無効にする" 79 | }, 80 | "image_size": { 81 | "message": "サイズ" 82 | }, 83 | "image_source": { 84 | "message": "ソース" 85 | }, 86 | "image_type": { 87 | "message": "タイプ" 88 | }, 89 | "image_unlazy_desc": { 90 | "message": "一部のウェブページでは、遅延読み込みを利用して非常に大きな画像を提供しています(例: r/EarthPorn)。
遅延読み込み解除を無効にしたい場合は、ここにドメインを追加してください" 91 | }, 92 | "image_unlazy_disable_list": { 93 | "message": "遅延読み込み解除を無効にする" 94 | }, 95 | "kofi_link": { 96 | "message": "Support me" 97 | }, 98 | "min_height": { 99 | "message": "最小高度" 100 | }, 101 | "min_width": { 102 | "message": "最小横幅" 103 | }, 104 | "new_option": { 105 | "message": "新しいオプションが設定可能です" 106 | }, 107 | "not_factor_of_360": { 108 | "message": "360の約数ではありません" 109 | }, 110 | "options": { 111 | "message": "オプション" 112 | }, 113 | "options_title": { 114 | "message": "オプション - Image Viewer" 115 | }, 116 | "popup_title": { 117 | "message": "リリースノート - Image Viewer" 118 | }, 119 | "release_notes": { 120 | "message": "リリースノート" 121 | }, 122 | "reset": { 123 | "message": "リセット" 124 | }, 125 | "rotate_degree": { 126 | "message": "回転の角度" 127 | }, 128 | "save": { 129 | "message": "保存" 130 | }, 131 | "scroll_hotkey": { 132 | "message": "アクティブ自動スクロール有効" 133 | }, 134 | "search_hotkey": { 135 | "message": "画像検索ホットキー" 136 | }, 137 | "size_filter": { 138 | "message": "画像サイズフィルター" 139 | }, 140 | "speed_control": { 141 | "message": "速度制御" 142 | }, 143 | "support": { 144 | "message": "サポート" 145 | }, 146 | "support_title": { 147 | "message": "サポート - Image Viewer" 148 | }, 149 | "svg_filter": { 150 | "message": "svgをフィルターする" 151 | }, 152 | "throttle_desc": { 153 | "message": "「矢印キーを長押し」すると、Image Viewerは各画像が切り替わる前に少なくともこの期間、各画像を表示します
デフォルト設定の 80 ms = 12.5 fps(1 秒あたり 12.5 画像)
スロットルタイムが 0 に近い値に設定されている場合、実際の表示速度はコンピューターの性能に依存します" 154 | }, 155 | "throttle_period": { 156 | "message": "スロットルタイム" 157 | }, 158 | "use_all_search": { 159 | "message": "上記すべて" 160 | }, 161 | "view_all_images_in_image_viewer": { 162 | "message": "画像を表示(フィルターなし)" 163 | }, 164 | "view_canvas_in_image_viewer": { 165 | "message": "Image Viewerでキャンバスを表示する" 166 | }, 167 | "view_images_in_image_viewer": { 168 | "message": "Image Viewerで画像を表示する" 169 | }, 170 | "view_last_right_click_image_in_image_viewer": { 171 | "message": "画像を表示(最後に右クリックした画像)" 172 | }, 173 | "width": { 174 | "message": "横幅" 175 | }, 176 | "zoom_ratio": { 177 | "message": "ズームの倍率" 178 | } 179 | } -------------------------------------------------------------------------------- /_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_new_custom": { 3 | "message": "新增自定" 4 | }, 5 | "app_desc": { 6 | "message": "更多功能、更方便的图像浏览" 7 | }, 8 | "app_name": { 9 | "message": "Image Viewer" 10 | }, 11 | "auto_desc": { 12 | "message": "当你使用自动翻页时,Image Viewer将会等待此时间再翻页到下一张图像
当翻页间隔被设置至接近0,实际翻页速度将取决于计算机性能" 13 | }, 14 | "auto_period": { 15 | "message": "自动翻页间隔" 16 | }, 17 | "auto_scroll_desc": { 18 | "message": "一些网页在向下滚动时会加载更多内容
如果要让Image Viewer通过主动式自动滚动加载新内容,请在此处添加域名" 19 | }, 20 | "auto_scroll_enable_list": { 21 | "message": "启用自动滚动" 22 | }, 23 | "custom_search": { 24 | "message": "自定" 25 | }, 26 | "custom_search_url": { 27 | "message": "自定URL" 28 | }, 29 | "debounce_desc": { 30 | "message": "长按箭头键时,Image Viewer会在越过最前/最后的图像前停留
当延时结束且未被其他操作中断,刚才的操作将会继续
设置延时时间为0以停止此行为" 31 | }, 32 | "debounce_period": { 33 | "message": "延时时间" 34 | }, 35 | "default_image_fit": { 36 | "message": "预设图像大小" 37 | }, 38 | "domain_settings_example": { 39 | "message": "示例设置(支持正则表达式):
example.com
sub.example.com
another.com
/^https.*regex\\.com/" 40 | }, 41 | "domain_specific": { 42 | "message": "网域特定设置" 43 | }, 44 | "download_hotkey": { 45 | "message": "下载已收集的图片" 46 | }, 47 | "fit_with_original_size": { 48 | "message": "图像原本的大小" 49 | }, 50 | "fit_with_original_size_not_exceeds_window": { 51 | "message": "图像原本的大小但不大於视窗" 52 | }, 53 | "fit_with_window": { 54 | "message": "视窗大小" 55 | }, 56 | "fit_with_window_height": { 57 | "message": "视窗高度" 58 | }, 59 | "fit_with_window_width": { 60 | "message": "视窗阔度" 61 | }, 62 | "function_hotkey": { 63 | "message": "功能热键" 64 | }, 65 | "github_link": { 66 | "message": "Github 用户手册(Eng)" 67 | }, 68 | "height": { 69 | "message": "高度" 70 | }, 71 | "hotkey_notice": { 72 | "message": "多个搜索使用相同的快捷键时,触发后会同时启动,既有快捷键如'Ctrl + W' 'Ctrl + T'等不会被覆盖

使用自定搜索引擎时,搜索图片的URL会置换'{imgSrc}'成真正的图像连结

软性禁止没有使用Ctrl/Alt/Shift的热键,在DevTools更改成需要的键以保存" 73 | }, 74 | "hover_check_desc": { 75 | "message": "Image Viewer允许您通过右键來选择图像
为了选择视频缩略图,默认情况下会删除悬停元素
如果要保留悬停元素,请在此处添加域名" 76 | }, 77 | "hover_disable_list": { 78 | "message": "禁用悬停检查" 79 | }, 80 | "image_size": { 81 | "message": "尺寸" 82 | }, 83 | "image_source": { 84 | "message": "來源" 85 | }, 86 | "image_type": { 87 | "message": "类型" 88 | }, 89 | "image_unlazy_desc": { 90 | "message": "一些网页通过懒加载來加载非常大的图片(例:r/EarthPorn)
如果希望禁用懒加载解除,请在这里添加域名" 91 | }, 92 | "image_unlazy_disable_list": { 93 | "message": "禁用懒加载解除" 94 | }, 95 | "kofi_link": { 96 | "message": "Support me" 97 | }, 98 | "min_height": { 99 | "message": "最小高度" 100 | }, 101 | "min_width": { 102 | "message": "最小阔度" 103 | }, 104 | "new_option": { 105 | "message": "有新选项可用" 106 | }, 107 | "not_factor_of_360": { 108 | "message": "不是360的因数" 109 | }, 110 | "options": { 111 | "message": "设定" 112 | }, 113 | "options_title": { 114 | "message": "设定 - Image Viewer" 115 | }, 116 | "popup_title": { 117 | "message": "发布版本通知 - Image Viewer" 118 | }, 119 | "release_notes": { 120 | "message": "发布版本通知" 121 | }, 122 | "reset": { 123 | "message": "重置" 124 | }, 125 | "rotate_degree": { 126 | "message": "旋转角度" 127 | }, 128 | "save": { 129 | "message": "保存" 130 | }, 131 | "scroll_hotkey": { 132 | "message": "启用主动式自动卷动" 133 | }, 134 | "search_hotkey": { 135 | "message": "图像搜索热键" 136 | }, 137 | "size_filter": { 138 | "message": "图像大小过滤" 139 | }, 140 | "speed_control": { 141 | "message": "速度控制" 142 | }, 143 | "support": { 144 | "message": "支持" 145 | }, 146 | "support_title": { 147 | "message": "支持 - Image Viewer" 148 | }, 149 | "svg_filter": { 150 | "message": "过滤svg" 151 | }, 152 | "throttle_desc": { 153 | "message": "长按箭头键时,Image Viewer会在切换每张图像前至少停留此时间
默认设定的 80ms = 12.5fps (每秒12.5张图片)
当节流时间被设置至接近0,实际翻页速度将取决于计算机性能" 154 | }, 155 | "throttle_period": { 156 | "message": "节流时间" 157 | }, 158 | "use_all_search": { 159 | "message": "以上所有" 160 | }, 161 | "view_all_images_in_image_viewer": { 162 | "message": "观看图片(无过滤)" 163 | }, 164 | "view_canvas_in_image_viewer": { 165 | "message": "在Image Viewer观看画布" 166 | }, 167 | "view_images_in_image_viewer": { 168 | "message": "在Image Viewer观看图片" 169 | }, 170 | "view_last_right_click_image_in_image_viewer": { 171 | "message": "观看图片(最后右键的图片)" 172 | }, 173 | "width": { 174 | "message": "阔度" 175 | }, 176 | "zoom_ratio": { 177 | "message": "缩放速度" 178 | } 179 | } -------------------------------------------------------------------------------- /_locales/zh_TW/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_new_custom": { 3 | "message": "新增自定" 4 | }, 5 | "app_desc": { 6 | "message": "更多功能、更易使用的圖像瀏覽" 7 | }, 8 | "app_name": { 9 | "message": "Image Viewer" 10 | }, 11 | "auto_desc": { 12 | "message": "當你使用自動翻頁時,Image Viewer將會等待此時間再翻頁到下一張圖像
當翻頁間隔被設置至接近0,實際翻頁速度將取決於電腦性能" 13 | }, 14 | "auto_period": { 15 | "message": "自動翻頁間隔" 16 | }, 17 | "auto_scroll_desc": { 18 | "message": "一些網頁會在往下捲動時載入更多內容
如果您想讓Image Viewer通過主動式自動捲動載入內容,請在此處添加域名" 19 | }, 20 | "auto_scroll_enable_list": { 21 | "message": "啟用自動捲動" 22 | }, 23 | "custom_search": { 24 | "message": "自定" 25 | }, 26 | "custom_search_url": { 27 | "message": "自定URL" 28 | }, 29 | "debounce_desc": { 30 | "message": "長按箭頭鍵時,Image Viewer會在越過最前/最後的圖像前停留
當延時結束且未被其他操作中斷,剛才的操作將會繼續
設置延時時間為0以停止此行為" 31 | }, 32 | "debounce_period": { 33 | "message": "延時時間" 34 | }, 35 | "default_image_fit": { 36 | "message": "預設圖像大小" 37 | }, 38 | "domain_settings_example": { 39 | "message": "範例設定(支援正則表示式):
example.com
sub.example.com
another.com
/^https.*regex\\.com/" 40 | }, 41 | "domain_specific": { 42 | "message": "網域特定設定" 43 | }, 44 | "download_hotkey": { 45 | "message": "下載已收集的圖片" 46 | }, 47 | "fit_with_original_size": { 48 | "message": "圖像原本的大小" 49 | }, 50 | "fit_with_original_size_not_exceeds_window": { 51 | "message": "圖像原本的大小但不大於視窗" 52 | }, 53 | "fit_with_window": { 54 | "message": "視窗大小" 55 | }, 56 | "fit_with_window_height": { 57 | "message": "視窗高度" 58 | }, 59 | "fit_with_window_width": { 60 | "message": "視窗闊度" 61 | }, 62 | "function_hotkey": { 63 | "message": "功能快捷鍵" 64 | }, 65 | "github_link": { 66 | "message": "Github 用戶手冊(Eng)" 67 | }, 68 | "height": { 69 | "message": "高度" 70 | }, 71 | "hotkey_notice": { 72 | "message": "多個搜索使用相同的快捷鍵時,觸發後會同時啟動,既有快捷鍵如'Ctrl + W' 'Ctrl + T'等不會被覆蓋

使用自定搜索引擎時,搜索圖片的URL會置換'{imgSrc}'成真正的圖像連結

軟性禁止沒有使用Ctrl/Alt/Shift的熱鍵,在DevTools更改成需要的鍵以保存" 73 | }, 74 | "hover_check_desc": { 75 | "message": "Image Viewer允許您透過按滑鼠右鍵來選擇圖像
為了選擇影片縮圖,預設情況下會移除懸停元素
如果要保留懸停元素,請在此處添加域名" 76 | }, 77 | "hover_disable_list": { 78 | "message": "停用懸停檢查" 79 | }, 80 | "image_size": { 81 | "message": "尺寸" 82 | }, 83 | "image_source": { 84 | "message": "來源" 85 | }, 86 | "image_type": { 87 | "message": "類型" 88 | }, 89 | "image_unlazy_desc": { 90 | "message": "一些網頁通過延遲載入來載入非常大的圖片(例:r/EarthPorn)
如果希望禁用反延遲載入,請在這裡添加域名" 91 | }, 92 | "image_unlazy_disable_list": { 93 | "message": "禁用反延遲載入" 94 | }, 95 | "kofi_link": { 96 | "message": "Support me" 97 | }, 98 | "min_height": { 99 | "message": "最小高度" 100 | }, 101 | "min_width": { 102 | "message": "最小闊度" 103 | }, 104 | "new_option": { 105 | "message": "有新選項可用" 106 | }, 107 | "not_factor_of_360": { 108 | "message": "不是360的因數" 109 | }, 110 | "options": { 111 | "message": "設定" 112 | }, 113 | "options_title": { 114 | "message": "設定 - Image Viewer" 115 | }, 116 | "popup_title": { 117 | "message": "發佈版本通知 - Image Viewer" 118 | }, 119 | "release_notes": { 120 | "message": "發佈版本通知" 121 | }, 122 | "reset": { 123 | "message": "重設" 124 | }, 125 | "rotate_degree": { 126 | "message": "旋轉角度" 127 | }, 128 | "save": { 129 | "message": "保存" 130 | }, 131 | "scroll_hotkey": { 132 | "message": "啟用主動式自動捲動" 133 | }, 134 | "search_hotkey": { 135 | "message": "圖像搜索快捷鍵" 136 | }, 137 | "size_filter": { 138 | "message": "圖像大小過濾" 139 | }, 140 | "speed_control": { 141 | "message": "速度控制" 142 | }, 143 | "support": { 144 | "message": "支援" 145 | }, 146 | "support_title": { 147 | "message": "支援 - Image Viewer" 148 | }, 149 | "svg_filter": { 150 | "message": "過濾svg" 151 | }, 152 | "throttle_desc": { 153 | "message": "長按箭頭鍵時,Image Viewer會在切換每張圖像前至少停留此時間
默認設定的 80ms = 12.5fps (每秒12.5張圖片)
當節流時間被設置至接近0,實際翻頁速度將取決於電腦性能" 154 | }, 155 | "throttle_period": { 156 | "message": "節流時間" 157 | }, 158 | "use_all_search": { 159 | "message": "以上所有" 160 | }, 161 | "view_all_images_in_image_viewer": { 162 | "message": "觀看圖片(無過濾)" 163 | }, 164 | "view_canvas_in_image_viewer": { 165 | "message": "在Image Viewer觀看畫布" 166 | }, 167 | "view_images_in_image_viewer": { 168 | "message": "在Image Viewer觀看圖像" 169 | }, 170 | "view_last_right_click_image_in_image_viewer": { 171 | "message": "觀看圖片(最後右鍵的圖片)" 172 | }, 173 | "width": { 174 | "message": "闊度" 175 | }, 176 | "zoom_ratio": { 177 | "message": "縮放倍率" 178 | } 179 | } -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | // utility function 2 | const srcBitSizeMap = new Map() 3 | const srcLocalRealSizeMap = new Map() 4 | const srcLocalRealSizeResolveMap = new Map() 5 | const srcLocalUrlMap = new Map() 6 | const redirectUrlMap = new Map() 7 | const tabSubtreeMap = new Map() 8 | const semaphore = (() => { 9 | // parallel fetch 10 | let activeCount = 0 11 | const maxConcurrent = 32 12 | const queue = [] 13 | return { 14 | acquire: function () { 15 | let executed = false 16 | const release = () => { 17 | if (executed) return 18 | executed = true 19 | activeCount-- 20 | const grantAccess = queue.shift() 21 | if (grantAccess) grantAccess() 22 | } 23 | 24 | if (activeCount < maxConcurrent) { 25 | activeCount++ 26 | return release 27 | } 28 | const {promise, resolve} = Promise.withResolvers() 29 | const grantAccess = () => { 30 | activeCount++ 31 | resolve(release) 32 | } 33 | queue.push(grantAccess) 34 | return promise 35 | } 36 | } 37 | })() 38 | 39 | const i18n = tag => chrome.i18n.getMessage(tag) 40 | const oldExecuteScript = chrome.scripting.executeScript 41 | chrome.scripting.executeScript = async function () { 42 | try { 43 | const result = await oldExecuteScript.apply(this, arguments) 44 | return result 45 | } catch (error) { 46 | return error 47 | } 48 | } 49 | 50 | function passOptionToTab(id, option, frameIds = undefined) { 51 | return chrome.scripting.executeScript({ 52 | args: [option], 53 | target: {tabId: id, frameIds: frameIds}, 54 | func: option => (window.ImageViewerOption = option) 55 | }) 56 | } 57 | 58 | async function fetchBitSize(src, useGetMethod = false) { 59 | const release = await semaphore.acquire() 60 | const method = useGetMethod ? 'GET' : 'HEAD' 61 | try { 62 | const res = await fetch(src, {method: method, signal: AbortSignal.timeout(5000)}) 63 | if (!res.ok) { 64 | return !useGetMethod ? fetchBitSize(src, true) : 0 65 | } 66 | 67 | if (res.redirected) { 68 | const originalPath = new URL(src).pathname 69 | const newPath = new URL(res.url).pathname 70 | if (originalPath !== newPath) return 0 71 | } 72 | 73 | const type = res.headers.get('Content-Type') 74 | if (!type?.startsWith('image')) { 75 | return !useGetMethod ? fetchBitSize(src, true) : 0 76 | } 77 | 78 | const length = res.headers.get('Content-Length') 79 | // may be transfer-encoding: chunked 80 | if (length === null) { 81 | const res = await fetch(src, {signal: AbortSignal.timeout(5000)}) 82 | if (!res.ok) return 0 83 | let totalSize = 0 84 | const reader = res.body.getReader() 85 | while (true) { 86 | const {done, value} = await reader.read() 87 | if (done) break 88 | totalSize += value.length 89 | } 90 | return totalSize 91 | } 92 | 93 | const size = Number(length) 94 | // some server return strange content length for HEAD method 95 | if (size < 100 && !useGetMethod) { 96 | return fetchBitSize(src, true) 97 | } 98 | return size 99 | } catch (error) { 100 | return 0 101 | } finally { 102 | release() 103 | } 104 | } 105 | async function getImageBitSize(src) { 106 | const cache = srcBitSizeMap.get(src) 107 | if (cache !== undefined) return cache 108 | 109 | const promise = fetchBitSize(src) 110 | srcBitSizeMap.set(src, promise) 111 | return promise 112 | } 113 | async function getImageLocalRealSize(id, src) { 114 | const cache = srcLocalRealSizeMap.get(src) 115 | if (cache !== undefined) return cache 116 | 117 | const release = await semaphore.acquire() 118 | const promise = new Promise(_resolve => { 119 | const resolve = size => { 120 | srcLocalRealSizeMap.set(src, size) 121 | _resolve(size) 122 | release() 123 | } 124 | srcLocalRealSizeResolveMap.set(src, resolve) 125 | 126 | chrome.scripting.executeScript({ 127 | args: [src], 128 | target: {tabId: id}, 129 | func: src => { 130 | const img = new Image() 131 | img.onload = () => chrome.runtime.sendMessage({msg: 'reply_local_size', src: src, size: img.naturalWidth}) 132 | img.onerror = () => chrome.runtime.sendMessage({msg: 'reply_local_size', src: src, size: 0}) 133 | setTimeout(() => img.complete || chrome.runtime.sendMessage({msg: 'reply_local_size', src: src, size: 0}), 10000) 134 | img.src = src 135 | } 136 | }) 137 | }) 138 | 139 | srcLocalRealSizeMap.set(src, promise) 140 | return promise 141 | } 142 | async function fetchDataUrl(src) { 143 | const release = await semaphore.acquire() 144 | try { 145 | const res = await fetch(src, {signal: AbortSignal.timeout(10000)}) 146 | const blob = await res.blob() 147 | return new Promise(resolve => { 148 | const reader = new FileReader() 149 | reader.onload = e => resolve(e.target.result) 150 | reader.onerror = () => resolve('') 151 | reader.readAsDataURL(blob) 152 | }) 153 | } catch (error) { 154 | console.log(`Failed to load ${src}`) 155 | return '' 156 | } finally { 157 | release() 158 | } 159 | } 160 | async function getLocalUrl(tabId, src) { 161 | if (src.startsWith('data:')) return src 162 | 163 | const cache = srcLocalUrlMap.get(src) 164 | if (cache !== undefined) return cache 165 | 166 | const size = await getImageLocalRealSize(tabId, src) 167 | if (size) { 168 | srcLocalUrlMap.set(src, src) 169 | return src 170 | } 171 | 172 | const dataUrl = await fetchDataUrl(src) 173 | srcLocalUrlMap.set(src, dataUrl) 174 | return dataUrl 175 | } 176 | async function getRedirectUrl(url) { 177 | if (url === '' || url === 'about:blank' || url.startsWith('javascript')) return url 178 | 179 | const cache = redirectUrlMap.get(url) 180 | if (cache !== undefined) return cache 181 | 182 | try { 183 | const res = await fetch(url) 184 | const finalUrl = res.redirected ? res.url : url 185 | redirectUrlMap.set(url, finalUrl) 186 | return finalUrl 187 | } catch (error) {} 188 | 189 | redirectUrlMap.set(url, url) 190 | return url 191 | } 192 | async function openNewTab(senderTab, url) { 193 | const subtree = tabSubtreeMap.get(senderTab.id) 194 | if (subtree === undefined) { 195 | const newTab = await chrome.tabs.create({active: false, index: senderTab.index + 1, url: url}) 196 | tabSubtreeMap.set(senderTab.id, [newTab.id]) 197 | return 198 | } 199 | const tabList = await chrome.tabs.query({windowId: senderTab.windowId}) 200 | const checkRange = Math.min(tabList.length, senderTab.index + subtree.length + 1) 201 | for (let i = senderTab.index + 1; i < checkRange; i++) { 202 | const tab = tabList[i] 203 | if (!subtree.includes(tab.id)) { 204 | subtree.length = i - senderTab.index - 1 205 | const newTab = await chrome.tabs.create({active: false, index: i, url: url}) 206 | subtree.push(newTab.id) 207 | return 208 | } 209 | } 210 | const newTab = await chrome.tabs.create({active: false, index: senderTab.index + subtree.length + 1, url: url}) 211 | subtree.push(newTab.id) 212 | } 213 | 214 | // main function 215 | const defaultOptions = { 216 | fitMode: 'both', 217 | zoomRatio: 1.2, 218 | rotateDeg: 15, 219 | minWidth: 180, 220 | minHeight: 150, 221 | svgFilter: true, 222 | debouncePeriod: 1500, 223 | throttlePeriod: 80, 224 | autoPeriod: 2000, 225 | searchHotkey: ['Shift + Q', 'Shift + W', 'Shift + A', 'Shift + S', 'Ctrl + Shift + Q', ''], 226 | customUrl: ['https://example.com/search?query={imgSrc}&option=example_option'], 227 | functionHotkey: ['Shift + R', 'Shift + D'], 228 | hoverCheckDisableList: [], 229 | autoScrollEnableList: ['x.com', 'www.instagram.com', 'www.facebook.com'], 230 | imageUnlazyDisableList: [] 231 | } 232 | 233 | let currOptions = defaultOptions 234 | let currOptionsWithoutSize = defaultOptions 235 | let lastImageNodeInfo = ['', 0] 236 | let lastImageNodeInfoID = 0 237 | 238 | chrome.runtime.onInstalled.addListener(details => { 239 | if (details.reason === 'update' || details.reason === 'install') { 240 | chrome.windows.create({url: '/page/popup.html', type: 'popup'}) 241 | } 242 | }) 243 | 244 | function resetLocalStorage() { 245 | chrome.storage.sync.get('options', res => { 246 | if (res && Object.keys(res).length === 0 && Object.getPrototypeOf(res) === Object.prototype) { 247 | chrome.storage.sync.set({options: defaultOptions}, () => { 248 | console.log('Set options to default options') 249 | console.log(defaultOptions) 250 | }) 251 | chrome.runtime.openOptionsPage() 252 | } else { 253 | currOptions = res.options 254 | console.log('Loaded options from storage') 255 | console.log(res.options) 256 | 257 | const existNewOptions = Object.keys(defaultOptions).some(key => key in currOptions === false) 258 | if (existNewOptions) { 259 | console.log('New options available') 260 | chrome.runtime.openOptionsPage() 261 | } 262 | } 263 | currOptionsWithoutSize = Object.assign({}, currOptions) 264 | currOptionsWithoutSize.minWidth = 0 265 | currOptionsWithoutSize.minHeight = 0 266 | }) 267 | } 268 | 269 | function addMessageHandler() { 270 | chrome.runtime.onMessage.addListener((request, sender, _sendResponse) => { 271 | if (!sender.tab) return 272 | 273 | const type = request.msg || request 274 | console.log('Messages: ', sender.tab.id, type) 275 | 276 | const sendResponse = (data = null, display = true) => { 277 | const msg = ['Response: ', sender.tab.id, type] 278 | if (data && display) msg.push(data) 279 | console.log(...msg) 280 | _sendResponse(data) 281 | } 282 | 283 | switch (type) { 284 | // wake up 285 | case 'ping': { 286 | _sendResponse(true) 287 | return 288 | } 289 | // option 290 | case 'update_options': { 291 | ;(async () => { 292 | const res = await chrome.storage.sync.get('options') 293 | currOptions = res.options 294 | currOptionsWithoutSize = Object.assign({}, currOptions) 295 | currOptionsWithoutSize.minWidth = 0 296 | currOptionsWithoutSize.minHeight = 0 297 | console.log(currOptions) 298 | _sendResponse() 299 | })() 300 | return true 301 | } 302 | // init 303 | case 'get_options': { 304 | ;(async () => { 305 | await passOptionToTab(sender.tab.id, currOptions) 306 | _sendResponse() 307 | })() 308 | return true 309 | } 310 | case 'load_worker': { 311 | chrome.scripting.executeScript({target: {tabId: sender.tab.id, frameIds: [sender.frameId]}, files: ['/scripts/activate-worker.js']}) 312 | _sendResponse() 313 | return 314 | } 315 | case 'load_extractor': { 316 | passOptionToTab(sender.tab.id, currOptions, [sender.frameId]) 317 | chrome.scripting.executeScript({target: {tabId: sender.tab.id, frameIds: [sender.frameId]}, files: ['/scripts/activate-worker.js', '/scripts/extract-iframe.js']}) 318 | _sendResponse() 319 | return 320 | } 321 | case 'load_utility': { 322 | ;(async () => { 323 | await chrome.scripting.executeScript({target: {tabId: sender.tab.id}, files: ['/scripts/utility.js', 'image-viewer.js']}) 324 | _sendResponse() 325 | })() 326 | return true 327 | } 328 | case 'load_script': { 329 | ;(async () => { 330 | await chrome.scripting.executeScript({target: {tabId: sender.tab.id}, files: ['image-viewer.js']}) 331 | _sendResponse() 332 | })() 333 | return true 334 | } 335 | // worker 336 | case 'reset_dom': { 337 | chrome.scripting.executeScript({ 338 | target: {tabId: sender.tab.id}, 339 | func: () => (window.ImageViewerLastDom = null) 340 | }) 341 | _sendResponse() 342 | return 343 | } 344 | case 'update_info': { 345 | ;(async () => { 346 | lastImageNodeInfo = request.data 347 | lastImageNodeInfoID = sender.tab.id 348 | // get data url if CORS 349 | if (sender.tab.url !== sender.url) { 350 | lastImageNodeInfo[0] = await getLocalUrl(sender.tab.id, lastImageNodeInfo[0]) 351 | } 352 | // image size maybe decreased in dataURL 353 | lastImageNodeInfo[1] -= 3 354 | console.table(lastImageNodeInfo) 355 | _sendResponse() 356 | })() 357 | return true 358 | } 359 | case 'get_info': { 360 | if (lastImageNodeInfoID === sender.tab.id) { 361 | sendResponse(lastImageNodeInfo) 362 | } else { 363 | sendResponse(['', 0]) 364 | } 365 | return 366 | } 367 | case 'reply_local_size': { 368 | const resolve = srcLocalRealSizeResolveMap.get(request.src) 369 | if (resolve) { 370 | resolve(request.size) 371 | srcLocalRealSizeResolveMap.delete(request.src) 372 | } 373 | _sendResponse() 374 | return 375 | } 376 | // utility 377 | case 'get_size': { 378 | ;(async () => { 379 | const size = await getImageBitSize(request.url) 380 | sendResponse(size, false) 381 | console.log(request.url, size) 382 | })() 383 | return true 384 | } 385 | case 'extract_frames': { 386 | ;(async () => { 387 | const newOptions = Object.assign({}, currOptions) 388 | newOptions.minWidth = request.minSize 389 | newOptions.minHeight = request.minSize 390 | if (request.canvasMode) newOptions.canvasMode = true 391 | 392 | // must use frameIds, allFrames: true wont works in most cases 393 | const frameList = await chrome.webNavigation.getAllFrames({tabId: sender.tab.id}) 394 | if (frameList === null || frameList.length < 2) { 395 | sendResponse([]) 396 | } 397 | const iframeIdList = frameList.slice(1).map(frame => frame.frameId) 398 | const func = async option => await window.ImageViewerExtractor?.extractImage(option) 399 | const results = await chrome.scripting.executeScript({ 400 | args: [newOptions], 401 | target: {tabId: sender.tab.id, frameIds: iframeIdList}, 402 | func: func 403 | }) 404 | if (results instanceof Error) { 405 | sendResponse([]) 406 | return 407 | } 408 | 409 | const relation = new Map() 410 | const pageDataList = [] 411 | const asyncList = [] 412 | for (const result of results) { 413 | if (!result.result) continue 414 | const [href, subHrefList, imageList] = result.result 415 | for (const subHref of subHrefList) { 416 | if (subHref !== href) relation.set(subHref, href) 417 | } 418 | const localImageList = imageList.map(src => getLocalUrl(sender.tab.id, src)) 419 | pageDataList.push([href, localImageList]) 420 | asyncList.push(localImageList) 421 | } 422 | 423 | await Promise.all(asyncList.flat()) 424 | 425 | const result = [] 426 | for (const [href, asyncList] of pageDataList) { 427 | let top = href 428 | while (relation.has(top)) top = relation.get(top) 429 | const imageList = await Promise.all(asyncList) 430 | for (const image of imageList) { 431 | result.push([image, top]) 432 | } 433 | } 434 | sendResponse(result) 435 | })() 436 | return true 437 | } 438 | case 'get_redirect': { 439 | ;(async () => { 440 | const resultList = await Promise.all(request.data.map(getRedirectUrl)) 441 | sendResponse(resultList) 442 | })() 443 | return true 444 | } 445 | case 'is_file_image': { 446 | ;(async () => { 447 | const asyncList = request.urlList.map(url => fetch(url, {method: 'HEAD'}).then(res => (res.headers.get('Content-Type')?.startsWith('image') ? 1 : 0))) 448 | const result = await Promise.all(asyncList) 449 | sendResponse(result) 450 | })() 451 | return true 452 | } 453 | // image viewer 454 | case 'open_tab': { 455 | openNewTab(sender.tab, request.url) 456 | _sendResponse() 457 | return 458 | } 459 | case 'close_tab': { 460 | chrome.tabs.remove(sender.tab.id) 461 | _sendResponse() 462 | return 463 | } 464 | case 'google_search': { 465 | ;(async () => { 466 | const blob = await fetch(request.src).then(res => res.blob()) 467 | const arrayBuffer = await blob.arrayBuffer() 468 | const dataUrl = await new Promise(resolve => { 469 | const reader = new FileReader() 470 | reader.onload = () => resolve(reader.result) 471 | reader.readAsDataURL(blob) 472 | }) 473 | 474 | const endpoint = 'https://www.google.com/searchbyimage/upload' 475 | const form = new FormData() 476 | form.append('encoded_image', new File([arrayBuffer], 'iv-image.jpg', {type: blob.type})) 477 | form.append('image_url', dataUrl) 478 | form.append('sbisrc', 'Image Viewer') 479 | 480 | const res = await fetch(endpoint, {method: 'POST', body: form}) 481 | if (!res.ok) return 482 | 483 | openNewTab(sender.tab, res.url) 484 | _sendResponse() 485 | })() 486 | return true 487 | } 488 | // download 489 | case 'download_images': { 490 | chrome.scripting.executeScript({target: {tabId: sender.tab.id}, files: ['/scripts/download-images.js']}) 491 | _sendResponse() 492 | return 493 | } 494 | case 'request_cors_url': { 495 | ;(async () => { 496 | const release = await semaphore.acquire() 497 | const res = await fetch(request.url) 498 | release() 499 | const blob = await res.blob() 500 | const reader = new FileReader() 501 | const dataUrl = await new Promise(resolve => { 502 | reader.onload = () => resolve(reader.result) 503 | reader.readAsDataURL(blob) 504 | }) 505 | const mime = res.headers.get('content-type').split(';')[0] || 'image/jpeg' 506 | sendResponse([dataUrl, mime]) 507 | })() 508 | return true 509 | } 510 | } 511 | }) 512 | } 513 | 514 | function addToolbarIconHandler() { 515 | chrome.action.onClicked.addListener(async tab => { 516 | if (!tab.url) return 517 | const supported = tab.url.startsWith('http') || (tab.url.startsWith('file') && (await chrome.extension.isAllowedFileSchemeAccess())) 518 | if (!supported) return 519 | 520 | await passOptionToTab(tab.id, currOptions) 521 | const script = tab.url.startsWith('file') && tab.url.endsWith('/') ? '/scripts/action-folder.js' : '/scripts/action-page.js' 522 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: [script]}) 523 | }) 524 | } 525 | 526 | function createContextMenu() { 527 | chrome.contextMenus.removeAll(() => { 528 | chrome.contextMenus.create({ 529 | id: 'view_images_in_image_viewer', 530 | title: i18n('view_images_in_image_viewer'), 531 | contexts: ['all'] 532 | }) 533 | chrome.contextMenus.create({ 534 | id: 'view_all_image_in_image_viewer', 535 | title: i18n('view_all_images_in_image_viewer'), 536 | contexts: ['action'] 537 | }) 538 | chrome.contextMenus.create({ 539 | id: 'view_last_right_click_image_in_image_viewer', 540 | title: i18n('view_last_right_click_image_in_image_viewer'), 541 | contexts: ['action'] 542 | }) 543 | chrome.contextMenus.create({ 544 | id: 'view_canvas_in_image_viewer', 545 | title: i18n('view_canvas_in_image_viewer'), 546 | contexts: ['action'] 547 | }) 548 | }) 549 | 550 | chrome.contextMenus.onClicked.addListener(async (info, tab) => { 551 | if (!tab.url) return 552 | const supported = tab.url.startsWith('http') || (tab.url.startsWith('file') && (await chrome.extension.isAllowedFileSchemeAccess())) 553 | if (!supported) return 554 | 555 | if (tab.url.startsWith('file') && tab.url.endsWith('/')) { 556 | const targetOptions = info.menuItemId === 'view_all_image_in_image_viewer' ? currOptionsWithoutSize : currOptions 557 | await passOptionToTab(tab.id, targetOptions) 558 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-folder.js']}) 559 | return 560 | } 561 | switch (info.menuItemId) { 562 | case 'view_images_in_image_viewer': { 563 | await passOptionToTab(tab.id, currOptions) 564 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-image.js']}) 565 | break 566 | } 567 | case 'view_all_image_in_image_viewer': { 568 | await passOptionToTab(tab.id, currOptionsWithoutSize) 569 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-page.js']}) 570 | break 571 | } 572 | case 'view_last_right_click_image_in_image_viewer': { 573 | await passOptionToTab(tab.id, currOptions) 574 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-image.js']}) 575 | break 576 | } 577 | case 'view_canvas_in_image_viewer': { 578 | await passOptionToTab(tab.id, currOptions) 579 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-canvas.js']}) 580 | break 581 | } 582 | } 583 | }) 584 | } 585 | 586 | function addCommandHandler() { 587 | chrome.commands.onCommand.addListener(async (command, tab) => { 588 | if (!tab.url) return 589 | const supported = tab.url.startsWith('http') || (tab.url.startsWith('file') && (await chrome.extension.isAllowedFileSchemeAccess())) 590 | if (!supported) return 591 | 592 | if (tab.url.startsWith('file') && tab.url.endsWith('/')) { 593 | const targetOptions = command === 'open-image-viewer-without-size-filter' ? currOptionsWithoutSize : currOptions 594 | await passOptionToTab(tab.id, targetOptions) 595 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-folder.js']}) 596 | return 597 | } 598 | switch (command) { 599 | case 'open-image-viewer': { 600 | await passOptionToTab(tab.id, currOptions) 601 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-page.js']}) 602 | break 603 | } 604 | case 'open-image-viewer-without-size-filter': { 605 | await passOptionToTab(tab.id, currOptionsWithoutSize) 606 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-page.js']}) 607 | break 608 | } 609 | case 'open-image-viewer-in-canvases-mode': { 610 | await passOptionToTab(tab.id, currOptions) 611 | chrome.scripting.executeScript({target: {tabId: tab.id}, files: ['/scripts/action-canvas.js']}) 612 | break 613 | } 614 | } 615 | }) 616 | } 617 | 618 | function init() { 619 | resetLocalStorage() 620 | addMessageHandler() 621 | addToolbarIconHandler() 622 | createContextMenu() 623 | addCommandHandler() 624 | console.log('Init complete') 625 | } 626 | 627 | init() 628 | -------------------------------------------------------------------------------- /css/options.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Helvetica, Arial, sans-serif; 3 | } 4 | #title { 5 | padding-left: 70px; 6 | font-size: 24px; 7 | height: 50px; 8 | line-height: 50px; 9 | margin-bottom: 10px; 10 | background: url(/icon/icon.png) 5px center no-repeat; 11 | } 12 | 13 | nav { 14 | display: flex; 15 | align-items: center; 16 | position: relative; 17 | margin-bottom: 10px; 18 | font-size: 16px; 19 | } 20 | nav > div { 21 | height: 50px; 22 | border: 2px solid; 23 | margin-left: 5px; 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | display: flex; 29 | align-items: center; 30 | position: relative; 31 | background-color: #303030; 32 | } 33 | a > img { 34 | border-right: 2px solid; 35 | border-color: black; 36 | background-color: white; 37 | } 38 | a > span { 39 | color: orange; 40 | margin: 0 5px; 41 | } 42 | 43 | form, 44 | fieldset, 45 | ul { 46 | padding: 0; 47 | margin: 0; 48 | } 49 | fieldset { 50 | padding: 15px 20px; 51 | margin-bottom: 20px; 52 | } 53 | 54 | ul { 55 | list-style-type: none; 56 | } 57 | 58 | .option-list > li:not(:last-child) { 59 | margin-bottom: 10px; 60 | } 61 | #option-form li.desc { 62 | font-size: 13px; 63 | } 64 | 65 | label { 66 | display: inline-block; 67 | width: 180px; 68 | } 69 | input#zoomRatio, 70 | input#rotateDeg { 71 | vertical-align: middle; 72 | width: 180px; 73 | } 74 | input.customSearchUrl { 75 | width: 500px; 76 | } 77 | 78 | span#notFactor, 79 | li#debounceDesc, 80 | li#throttleDesc, 81 | li#autoDesc { 82 | display: none; 83 | } 84 | -------------------------------------------------------------------------------- /css/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Helvetica, Arial, sans-serif; 3 | padding: 0; 4 | overflow-y: scroll; 5 | font-size: 100%; 6 | } 7 | #title { 8 | height: 50px; 9 | line-height: 50px; 10 | padding-left: 70px; 11 | background: url(/icon/icon.png) 5px center no-repeat; 12 | font-size: 24px; 13 | margin-bottom: 10px; 14 | } 15 | 16 | nav { 17 | display: flex; 18 | align-items: center; 19 | position: relative; 20 | margin-bottom: 10px; 21 | } 22 | nav > div { 23 | height: 50px; 24 | border: 2px solid; 25 | margin-left: 5px; 26 | } 27 | 28 | a { 29 | text-decoration: none; 30 | display: flex; 31 | align-items: center; 32 | position: relative; 33 | background-color: #303030; 34 | } 35 | a > img { 36 | border-right: 2px solid; 37 | border-color: black; 38 | background-color: white; 39 | } 40 | a > span { 41 | color: orange; 42 | margin: 0 5px; 43 | } 44 | 45 | .note-container-group { 46 | padding: 20px 20px; 47 | } 48 | 49 | .note-container { 50 | white-space: pre; 51 | border: 2px solid; 52 | border-bottom: none; 53 | } 54 | .note-container:last-of-type { 55 | border-bottom: 2px solid; 56 | } 57 | 58 | .bar { 59 | background-color: rgb(120, 120, 120); 60 | color: white; 61 | width: 100%; 62 | height: 40px; 63 | border: none; 64 | cursor: pointer; 65 | font-family: Consolas, Monaco, monospace; 66 | text-align: left; 67 | font-size: 16px; 68 | } 69 | .bar:hover { 70 | background-color: orange; 71 | } 72 | .bar::after { 73 | content: '\02795'; /* Unicode character for "plus" sign (+) */ 74 | font-size: 15px; 75 | float: right; 76 | margin-left: 5px; 77 | } 78 | .active > .bar::after { 79 | content: '\2796'; /* Unicode character for "minus" sign (-) */ 80 | } 81 | 82 | .noteText { 83 | background-color: #eff5f7; 84 | overflow: hidden; 85 | overflow-x: auto; 86 | padding: 0 10px; 87 | max-height: 0; 88 | transition: max-height 0.2s ease-out; 89 | } 90 | .active > .noteText { 91 | transition: max-height 0.2s ease-in; 92 | } 93 | -------------------------------------------------------------------------------- /css/shadow-viewer.css: -------------------------------------------------------------------------------- 1 | /* Just file version of style() in image-viewer.js */ 2 | /* global */ 3 | :host { 4 | all: initial !important; 5 | } 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | color: #ddd; 10 | font-family: Verdana, Helvetica, Arial, sans-serif; 11 | user-select: none; 12 | -webkit-user-drag: none; 13 | } 14 | 15 | /* root container */ 16 | #image-viewer { 17 | position: fixed; 18 | top: 0; 19 | left: 0; 20 | z-index: 2147483647; 21 | width: 100%; 22 | height: 100%; 23 | background: rgba(0, 0, 0, 0.8) !important; 24 | } 25 | 26 | /* image list */ 27 | #iv-image-list { 28 | width: 100%; 29 | height: 100%; 30 | transition: 0s; 31 | } 32 | #iv-image-list li { 33 | position: absolute; 34 | cursor: move; 35 | width: 100%; 36 | height: 100%; 37 | display: flex; 38 | justify-content: center; 39 | align-items: center; 40 | overflow: hidden; 41 | translate: 100% 0; 42 | } 43 | #iv-image-list li.current { 44 | translate: 0 0; 45 | } 46 | img { 47 | max-width: 100%; 48 | max-height: 100%; 49 | transition: transform 0.05s linear; 50 | } 51 | img.loaded { 52 | max-width: none; 53 | max-height: none; 54 | } 55 | 56 | /* control panel */ 57 | #iv-control { 58 | position: fixed; 59 | bottom: 0; 60 | width: 100%; 61 | height: 60px; 62 | background: rgba(0, 0, 0, 0); 63 | } 64 | #iv-control * { 65 | opacity: 0; 66 | } 67 | #iv-control.show, 68 | #iv-control.show * { 69 | background: rgba(0, 0, 0, 0.8); 70 | opacity: 1; 71 | } 72 | #iv-control ul { 73 | height: 50px; 74 | margin: 5px 0; 75 | list-style: none; 76 | } 77 | #iv-control span { 78 | font-weight: normal; 79 | line-height: normal; 80 | } 81 | 82 | /* control panel buttons */ 83 | #iv-control button { 84 | cursor: pointer; 85 | position: relative; 86 | width: 50px; 87 | height: 50px; 88 | margin: 0 5px; 89 | border: 0; 90 | border-radius: 5px; 91 | box-shadow: inset 0 0 2px #fff; 92 | } 93 | #iv-control button:hover { 94 | box-shadow: inset 0 0 10px #fff; 95 | } 96 | #iv-control button:active, 97 | #iv-control button.on { 98 | box-shadow: inset 0 0 20px #fff; 99 | } 100 | 101 | /* control panel layout */ 102 | #iv-index { 103 | position: absolute; 104 | left: 10px; 105 | top: 0; 106 | display: none; 107 | opacity: 1; 108 | z-index: 1; 109 | } 110 | #iv-control-buttons { 111 | display: flex; 112 | justify-content: center; 113 | } 114 | #iv-info { 115 | position: absolute; 116 | right: 10px; 117 | top: 0; 118 | height: 44px !important; 119 | padding: 3px 0; 120 | } 121 | 122 | /* index */ 123 | #iv-index li { 124 | height: 50px; 125 | } 126 | #iv-counter { 127 | align-content: center; 128 | opacity: 1; 129 | } 130 | #iv-counter span { 131 | font-size: 20px; 132 | text-shadow: -1px -1px 0 #000, 0 -1px 0 #000, 1px -1px 0 #000, 1px 0 0 #000, 1px 1px 0 #000, 0 1px 0 #000, -1px 1px 0 #000, -1px 0 0 #000; 133 | opacity: 0.5; 134 | } 135 | 136 | /* image info */ 137 | #iv-info li { 138 | height: 22px; 139 | display: flex; 140 | } 141 | #iv-info span { 142 | font-size: 16px; 143 | margin: 0 2px; 144 | } 145 | #iv-info span:last-child { 146 | display: inline-block; 147 | width: 80px; 148 | text-align: center; 149 | border: 1px transparent dashed; 150 | border-radius: 5px; 151 | } 152 | #iv-info span:last-child:hover { 153 | border-color: #aaa; 154 | } 155 | 156 | /* info button */ 157 | #iv-control-info { 158 | cursor: pointer; 159 | position: absolute; 160 | left: -50px; 161 | top: -50px; 162 | width: 100px; 163 | height: 100px; 164 | background: #fff; 165 | border: 0; 166 | border-radius: 50%; 167 | box-shadow: inset 0 0 0 #fff; 168 | opacity: 0; 169 | } 170 | #iv-control-info.show { 171 | opacity: 0.8; 172 | } 173 | #iv-control-info::before { 174 | content: '\\2139'; 175 | position: absolute; 176 | right: 50%; 177 | margin-right: -26px; 178 | margin-top: -5px; 179 | font-size: 35px; 180 | color: #999; 181 | } 182 | 183 | /* info popup */ 184 | #iv-info-popup { 185 | cursor: pointer; 186 | display: none; 187 | position: fixed; 188 | top: 0; 189 | max-width: 70%; 190 | opacity: 0.9; 191 | background: #fff; 192 | border: 1px black solid; 193 | z-index: 1; 194 | } 195 | #iv-info-popup.show { 196 | display: flex; 197 | } 198 | #iv-info-popup-list { 199 | max-width: calc(100% - 10px); 200 | margin: 5px; 201 | list-style: none; 202 | line-break: anywhere; 203 | } 204 | #iv-info-popup-list * { 205 | color: #000; 206 | } 207 | #iv-info-popup-list span { 208 | margin-left: 4px; 209 | user-select: text; 210 | } 211 | 212 | /* close button */ 213 | #iv-control-close { 214 | cursor: pointer; 215 | position: absolute; 216 | right: -50px; 217 | top: -50px; 218 | width: 100px; 219 | height: 100px; 220 | background: #fff; 221 | border: 0; 222 | border-radius: 50%; 223 | box-shadow: inset 0 0 0 #fff; 224 | opacity: 0.8; 225 | visibility: hidden; 226 | } 227 | #iv-control-close.show { 228 | visibility: visible; 229 | } 230 | #iv-control-close::before, 231 | #iv-control-close::after { 232 | content: ''; 233 | position: absolute; 234 | left: 50%; 235 | margin-left: -20px; 236 | margin-top: 5px; 237 | background: #999; 238 | width: 5px; 239 | height: 30px; 240 | } 241 | #iv-control-close::before { 242 | transform: rotate(-45deg); 243 | } 244 | #iv-control-close::after { 245 | transform: rotate(45deg); 246 | } 247 | 248 | /* navigation button */ 249 | #iv-index button::after { 250 | content: ''; 251 | position: absolute; 252 | margin-top: -12px; 253 | display: block; 254 | border-style: solid; 255 | } 256 | #iv-control-prev::after { 257 | left: 50%; 258 | margin-left: -10px; 259 | border-width: 12px 18px 12px 0; 260 | border-color: transparent #787878 transparent transparent; 261 | } 262 | #iv-control-next::after { 263 | right: 50%; 264 | margin-right: -10px; 265 | border-width: 12px 0 12px 18px; 266 | border-color: transparent transparent transparent #787878; 267 | } 268 | 269 | /* control button */ 270 | #iv-control-both { 271 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpFMjlCMEFGMTRDQzZFMTExOEZFQUQ0QkNGMDJGMzg3NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCQ0YxQUQ0NEM2NTAxMUUxQjgzRUY4RjM0QUVGODRFQyIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCQ0YxQUQ0M0M2NTAxMUUxQjgzRUY4RjM0QUVGODRFQyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUzOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUyOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8++nwS+AAABB1JREFUeNrsnUtoE0EcxicaRaFKQQ8+oHqoGlvwcfNoRVBP2ouIl9ZLDz4OVqkGi1ZaWInaiyIiHrxo9eDjpMWL4llRRG2LIthDLVghYqk1BOM37ERimmQ3uzObbPJ98DFtMrPJ/n/7n53N7uxGMpmMoKpH8xgCAqEIhEAoj4qGfQXi8XgERQLusSwrwwypvPbDJ1UZekXCPOxFdixCMQKvhb/AMWTJLDOkcjquYEitgbuZIZXLjhUoxuClOS//hNcjSyaZIcGrPw+G1BL4K7us4LNjM4pDJd7fQiDBahCe7/A+gQSUHXtR7HCo1oZ6+wjEPIwFKC66rJ5A/YUEYlZH4XUu68p6RzjsNZcdy1B8ghvLaJaEmzEM/s4M0a++MmEIVb+PGaI/OzaieCu8/RiahjchS0aYIfp0WXj/ZTqq2jNDAsicgl8e2RDhcQhFIARCEQiBUARCEQiBUARCIBSBEAhFIARCEQhFIARCEQiBUD4VZQj8KR6P+2pvWZaZDMEXa4Gf1CETuc4tVZMhgLAcxXm4q04zbje8E74Bn4OnKgJEXch8DO4V5V9RWItd/2H4IDwAX4FTge3UAaMdxXv4EmH8p0YVExmbduMZAhBbhT0ZZjtjX1LN8AP4ubAnor7WCgQgVqpU7PSSVWOjo3Ne2xCLaa1XTZ+bI7nhvoRvqa7dcf5jyUtJAWKxInwabuCG70vT8AXVw/wqNuyNFgEhr409oBbQxFhqUYPqZbrUBn4XzrjdqUuKdwjDiJpUbAfLGWV1qyHcOOOnXeMqtt2ugci76sBD+DOmdkbTjKOWfUiviulQoe7KcaeeN8qaYEx9aVWhUZarnXqBRnJBETfHIZ0dHRHTQ81svWITdrLfwdTn5m2spbZoM8chOWDkgtvUkXpCHQA5fulC0l3PbRuTn5sjOVu4B34YyE8nACM/qFXYNw5Lsjf6p6SKSasXGJ6BKCgpWE6mlBP0rwl7tmu9Kq1iIGMhY5LyuiDf50MAZQqWd0yQd+gZrkMYw2rdZQym/C5M2/kLQPmAYk8dAtG6zjynXmXiOXX/PYNghtSwCIRAKAIhEIpACIQiEAKhCIRAKAKhCIRAKAIhEIpACIQikHpWWJ6O8Fj4u7rjqWVZu5gh+nRCeL8QL63as8vSJfXsj+sem99E+3cEol99ovzriH/AZ7lTN5Ml8jlS/WU2G0C7bwRiTlfhjy7rfhb2HRU47DWYJfKq8h6X1eUD738TiHkoj1A8c6j2AvXu88AwOMlpYn8c3ueReoBZ8kbYt6so9v4rAgleZ8Tc6dry/9UhXqfwAkEWTAr71h+5SuD1CQKpnOR8vuzdJsZFiB4gWZNAkA2zKE5luzD8PxN2ILUwg+oevA2+XQPrEu5Hr9aieIKKQCgCIRDKq/4KMACWrCf3M5jnFgAAAABJRU5ErkJggg==) !important; 272 | background-size: cover !important; 273 | } 274 | #iv-control-width { 275 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpFMjlCMEFGMTRDQzZFMTExOEZFQUQ0QkNGMDJGMzg3NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpDMUY5QUJENEM2NTAxMUUxOUIyQ0IyMkFFREYxRUMyRCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpDMUY5QUJEM0M2NTAxMUUxOUIyQ0IyMkFFREYxRUMyRCIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUzOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUyOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8++tXJrAAAAnNJREFUeNrsnM8rBGEch3ckpfbg4MLBiWwc5D9wcODoJhdOeyCXTbKlpNRKuCgHJyecOHL0BygnWuXkwGUPe1BqU+Pz1jeF/TG7M8tknqc+vZN2h/k+768xNZ7v+ymIDx2UACGAEIQAQhACCEEIIAQhgBBACEIAIQgBhCAEEIIQQAggBCGAEIQAQhACCEEIIAQQghBACEIAIQgBhCAEEAIIiTedlCAc+Xw+1PcLhUJ7Roj+sBHlMoFO3DWPxGaESEKvmk0lm9ARN6VMKkfKhlL6EyES0aVmWVlXepj6U4vKnLKlHCiVX1vUJWNGzZ2yi4wv9FhNXG1m2j5CJGJczb4yQe3rMqicK9dKTrmNVIhE9NlQXGhlVD0Uiz9+NpzJ/IvPNcB13Bvl2Kb2l0Zf8Oq9nkkius3wmpKm44fiVdm2Geat1ra3s4YIT82snWCAWkZC2maZrHXwM8UPuqg7iyfIaAsDVtv9ZnZZOdvCPVG/yHmy2uYCC9G85iunOszYYvRKHSNZQ9atpqfVpquGi/q3XdYzNQ1Ff7VdVqBFvcqX3Im8IPchC/Pz3n/d4lb7nGpSr0c3fR/itfJWUrtT37EboO/yvCR1+xpCHpVV5SJAZw//rxOdxP2iUWVFKTMbfVK2mowGkdHMLiuIlIqyp8Mh5VB5T7CId6uBq4WrSaXVE4V+HiIpJWVJh2PKVQJlXNm1uxqUwp4ssucXknKvZjqBQiK9Zp6pxwyPd7/HC0YIQgAhCAGEIAQQghBACEIAIYAQhABCEAIIQQggBCGAEEAIQgAhCAGEIAQQghBACCAEIYAQhABCEAIIQQggBBAScz4EGADyS6Iw76d4WwAAAABJRU5ErkJggg==) !important; 276 | background-size: cover !important; 277 | } 278 | #iv-control-height { 279 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpFMjlCMEFGMTRDQzZFMTExOEZFQUQ0QkNGMDJGMzg3NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCOTg0RTgyNEM2NTAxMUUxQTRGQ0VBQ0ZFNDI0NzUwNSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCOTg0RTgyM0M2NTAxMUUxQTRGQ0VBQ0ZFNDI0NzUwNSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUzOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUyOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+0DCtRAAAAndJREFUeNrsncFLFFEcx9/mBl0UoUvQIQ8aCVIdu8V2CU968u45L1YoixALBhMLdalDhw5ePPQPdOjQoXsgokgUgh5KECFRREWcvo99QSyxOzPNvPEtnw/8+O3OvCeuH76zs+vhV4nj2MDF4RJ/AoQAQhACGamG/gLq9XpFramai6IoJiHlM6V66nrwVEK+7VU6rqhtqIZUW6pbSskxCSmPWSfDckP1mISUl45ral9VA38dPlDdVEp2SIh/FttkWPpVP7lk+U/HHbXpDufvIsQvr1R9Xc4jxFM6JtQedFlW07pJ3tSLl3FZbV01kmD5N9WY3uBPSUhxzCSUYdy6RySkuHRcVfuuGkyx7ZdqWCnZIyH500gpw7j1DRKSfzpG1VZNti9Dz1S3lZINEpIfL032b6arbj8J8ZCcf/7ySkOFzyGAEIQAQhACCAGEIAQQghBACEIAIQgBhABCEAIIQQggBCGAEIQAQgAhCAGEIAQQghBACEIAIYAQhABCEAIIQQggBCGAEIQAQgAhCAGEIAQQghBACEIAIYAQhABCEAIIQQh4JJTpCB/Uxv/jR3yMoughCcmPJ6Y1diILZ24/l6y8cLM/3mbc/k771xCSPw3TGmGUhn3VM97Ui0mJnSO1mHLbc+3bRUhxvDGtcXhJ2FS95ra32JTYmYRzCZfbgfcn3Pb6uQ3+pFbrsOSzZNzng6E/7Nz08y7n+aTu8dK1orbU4fwXhPhnQXXYdsw+vx7wawpXiFKwo/ai7XBTx38gpDzswMht93jbBDRAsieFKA3HavN/LmF6fhS6kKoJn/eqe6rlHngtYY9e7UX4BxVCACEIgaz8FmAAavyUc1I71hUAAAAASUVORK5CYII=) !important; 280 | background-size: cover !important; 281 | } 282 | #iv-control-none { 283 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpFMjlCMEFGMTRDQzZFMTExOEZFQUQ0QkNGMDJGMzg3NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpCNTE3QTJGNEM2NTAxMUUxOTdBNjg0RjY1RThFQ0QwMiIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpCNTE3QTJGM0M2NTAxMUUxOTdBNjg0RjY1RThFQ0QwMiIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkUzOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkUyOUIwQUYxNENDNkUxMTE4RkVBRDRCQ0YwMkYzODc3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+EIJY5QAAASxJREFUeNrs3UEOgjAQQFFrvHRP0GMPrIkmQKG25f2lRqO80IEIMUXES/30tgmACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEDUrM/RF+Sca689TR1uh6rvVEpJ9hBLloAA0XBDfTvA1iF/y6Bs3N6hHPYQS5aACAgQAXnEYW/HxYnDWHvITRAx8DnQVCDxz5M4IIa6gAAZunTyOSCNUYY97J3lPCRN8j3MECACAkRAgAgIEAEBIiACAkRAgAjI9M3yA9X2kh8XynWE8euxIUpH/2Hngtuid32uCow73qe6vbdOmyGWLAEB0m4GXjA/xh7qX4b8ow9712HdF4gsWUAEBIiACAgQAQEiIEAEBIiACAgQAQEiIEAEBIiACAgQAQGiS1oEGACl7SnD1JcJ0wAAAABJRU5ErkJggg==) !important; 284 | background-size: cover !important; 285 | } 286 | #iv-control-moveto { 287 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAACjZJREFUeJztXXvsHUUVpjxanrVAi6VICQjhISCvIiohEARK7AMRbMQKtVahCJ8RJaiUgIlAeQghoBFSpZQAISoC8ghqrAQp2JLQUpA3BQyggsUXIEKp58uZX3Lz692d2Z0zO3fv5Uu+v+7dPWfm7M7jnDNn11uvhwFgA+FewnOEVwlPFX5ImFu1wYN0+ljhLcL/Ctd28B/Cc4Xr59ZxYCCdvZXw98MMMZzzhRvl1rXv4Yap6z3GIN8VfjW3vn0P6eSjXGf7DEK+IhyfW+e+hnTwHYHGGOLpuXXuW3D1JPx7RYMszK1330I6dwvhexUNsiS33n0L6dxdKhqDfCK33n0L6dydaxjkyQR6jBBOFB4hnCKcJpwuPCaQ/P9Ud+1k4UfQxn2TKL15jSHrPiPZ67s39NvCF2o8GD7ynjOFoy30bQyi8N8qNvQnBjI3FV4g/GsCQ3RyjfDXwr0t+qoRiLI/r9jIqM2hXL+fcHGNNzOGzwgPseqzpBBFD8O6/qsirhKOi5BFY7zcoCE6+arwYMu+SwJRckOEuU74RM9BTc+vXDdG+GAmYwyR80rvzymi5AeEd3oac7Zwg5r35+LhV5mNMcRr+RBa96E5RMlthD/FumP7G8JvIsLTK9deDJ1gcxuD5PB8pGXfJYUou6PwJOFpwmOFW0Xe79PCdyp22hp3zf9K+E4H30W4g5R8SLiZVZ8lB9Qlv7FwROR9uM/4S0AHLRWeLPwEdFO3u3C3AO4+jLx2mvBhjzwab4pVfyWBMwKf5vs6nuh/C38p3BcVJ3P5/2jhYwFvAvcjprtqud8o4Y0e2VdbyjSFM8alJUML55HPV7gfXSE/Dngz+J+Ridq0ifBPJbKfTSE3GlAX/Jnwb9TeFB4eeL9T4B/TuYPeNHHbji55yMgPp5RfC6LUnsJ/BjzN5CPCLTz3m+yGurL7rBZu20DbNoJ6BYr0CH7rGwM03afKCui4knuNhoZ5y65/W3hEg+2bU6LLeU3pEQxR6qmKBrm84D703t4ccP2ZDbdvD+gyuesc1qQuXkAn31A/1hBvK7jPZQHXXpehjdu6IbKbPjc2rU8pRKHNKhqDXCeEC137+ybxJ4UTMrRxgvD1Ap1uaFqfUkAdflUN8tCwe+wkfN5zDWMu+2dqI1Nji1ZaP8qhUyGgGYtVDfJwx/V0Sv4x4JrpGds4u0Svebn06gpoPm9Vg6xw13LeuCng/9ERxoj2caFR5sUuXDFmgSg0roZBVrprvwj/ZpL5wqMytu+j0A1tkX475NKtK2oahP6pj8O/Olsl3CZj2/gGl8VgnsqlWyGgMZCqBuHG7xnPf2is4zO37QsoX/ktyKlfV4hSH6xhEB+5ovmWoY4jhVu6h2c8dF8xwXG7DvJgEXO7GLdf4NGRXuapVjqaIZFBFsFg3oDGN+ZBk8GXQT23TwufFT7nyGGRS27Gyl8UvoSwjS59cptb9KEpEhiEHRblwYUmXZyOdCFfvsHHWvWhKdwQYNVQHn07KFIf8suBT3ld3mHxBieB4RvCDjzBQB/GL1YnNAbDDLta9F0SoN4qqxuvNNCFm9Q/JzQGh6qTLPotGYwMwgyOsZF6cM9QNaW1Kr+BXs+IR/yQxVjKTpE60BhXJDTEa8JZRl2WFgYGOcxAh+monrcVSu7UD0BkOlNjQNwqK9pTKvfYG7p3KJPzH+hwxrfoB44XF5Bn6c8Xfl04qTWGGEKEQW5BZPoOdPe9NEAWT0j1fi6uBWoahM7F0syTALnkzwJkzbdqaysA9QtVMQbTe6LPWMg9vgT/Tvzm2LewdahoEE68sw1kMn/3LY+s5UicRNeTqGgQTpa1zod0yGNmve8EFV0wB1q1sVWAurBDjLHCYBKnG/1Wjxwm0c2wal/rII3/TIAxmEIT5f+Bbv7ODpg3LkSv76ZTgcMCyuPNa91Y/0kDWbPh3/wxe2UwlrfDAXXkrQqZNwxkjUdxotoQeUJ2R4u2tQ7QjPCFAca4S7hJpCwe9vyDRw534ocaNa9dgG7IzgoYyx9FZMaImzduCDB8byWrNQk3ib8dMG/sFSmHnBtgjF+gV6N3qQGNfYQUKzvVQNbHAuYNJi5sadG21gGa5X5vgDGuMpC1AzQzxDdv7GzRttbBjeVXBhiDy86o1Bgn626PnPcs3sJWwo3lJwcYg9mIe0bKojHmBRjjAqv2tQ7QZGOf74ibw+i0T+gZd1/6zr2DPIkzrfLpgLcjuuwr9CTvGx45zC7svWPITcANH3cFGIPHBWKdhqxs+huPHMZRjrZqX6vgjHEG/Js/5spGnY+AlrD4oUcW542vYYCdhlMDxnLuR/YzkMWyTb7DnkzdHFinYWj1nbMMZLHE7L88cjiHDWateOiJ2sUBxrgGkZ+dgEYaH/fI4VJ6N6v2ZQfUbc3kYxYRmyE8AXqubxY0Q/wr0CIvHJ9Z+/Z38J/5ux/xBcl4kNK3YKAec636IhugIdXzhE8EPOlVyZTK7SP144LhewHGuASR8ffsgKZUPh7wlNfl5wx0PBL+eeMeROZtZQc7C8UFUix4roGOu8Jf6YcZI+2exKGnRX15SjG8HfGTOCvOLffI4fL3KKt+yQJoLcOUxqAbfGKkjpw3LvfI4TDb7m/rQWPbPld1DFcLDzDQcy78mz+W2Gj9JM55I9VJUwaAPmWgI9M+fSFfJl9nq9hgAqgPaFkCQ7DzuMrZP3YEgVb68R0X4CQ+yahb8gF65qFsecshgrEDbggPFR7sntaDoPHqSa7TWbVgH+jBFxYWNlluQoso+2q1U//+8OCi/GwEA0YnIqNDTmR/3/PAkPyiQrtOKHUDNKxa9GUZzilnZNaPb6Av7ZOfndg4p55mgBZKKWroEuStL7U9/Js/niVv9+avE27+KGpsdDWECL1Gw7/Q4J7ps7l0TAJp0HcLGsuyD1HOvwidQsq8ck75Tl/MG51AcY4UD+E3fqbOGePEgHljYc6FRjKguLgWv3mRQx8upV/1GIO1qKLKZ/QsSgyysunhABplfNFjDObo7tukXo0CxY46rl4a+4IY1IO7yGMMblBnNqVTFkDLPnRrPMfwRqo/u3ljfsAkfkkT+mQFNOpW1AnRgaRAHSYHTOI8+bR1E/pkBbS8d5E7e5Vwu8TyGfnzxez5e/9kjPjgmUivSCh3LPxn/vjmRJ+8bRVQ/qlT7obNd+xQD+6d8DsNL2p6tZcd0uBDUJ7UQCcjD0wyxFv3c6cjnBHonqd34DmPIchFdeW1GtAA1ZKADmIOLov+Lu/CFV34SAcZyeNHVF4LkLPW3XNM7r7JBuhqyxcebYr84Er7I38xgFZwTl2JM4QcOpmKNFjzRjdAP8fjqzeSmue8b4wOSGccj3yftmYoebCqtoVAOmUm0pVK7UbKugz96E63ADTOzjwtnxvcgoznM5tlMI+YVQG0sDG/oOzLMK9rCJ4jjzofMnCAbuh4pPkUN8Yvhn7j/H5HVlxY6uED0H3Ob52BpwjH5W5bL+L/sdiFy+uT9dcAAAAASUVORK5CYII=) !important; 288 | background-size: cover !important; 289 | } 290 | -------------------------------------------------------------------------------- /css/support.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Verdana, Helvetica, Arial, sans-serif; 3 | } 4 | #title { 5 | padding-left: 70px; 6 | font-size: 24px; 7 | height: 50px; 8 | line-height: 50px; 9 | margin-bottom: 10px; 10 | background: url(/icon/icon.png) 5px center no-repeat; 11 | } 12 | 13 | ul { 14 | font-size: 16px; 15 | list-style-type: none; 16 | padding: 0; 17 | margin: 0; 18 | } -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import path from 'node:path' 3 | import {fileURLToPath} from 'node:url' 4 | import js from '@eslint/js' 5 | import {FlatCompat} from '@eslint/eslintrc' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = path.dirname(__filename) 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all 13 | }) 14 | 15 | export default [ 16 | ...compat.extends('standard'), 17 | { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.webextensions, 22 | ImageViewer: false, 23 | ImageViewerUtils: false 24 | }, 25 | 26 | ecmaVersion: 'latest', 27 | sourceType: 'module' 28 | }, 29 | 30 | rules: { 31 | 'space-before-function-paren': 'off', 32 | 'object-curly-spacing': 'off', 33 | 'spaced-comment': 'off', 34 | 'object-shorthand': 'off', 35 | indent: 'off' 36 | } 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /icon/GitHub-Mark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hospotho/Image-Viewer/f2845cc62574ce0f1de061e4879ecb680392a365/icon/GitHub-Mark.png -------------------------------------------------------------------------------- /icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hospotho/Image-Viewer/f2845cc62574ce0f1de061e4879ecb680392a365/icon/icon.png -------------------------------------------------------------------------------- /icon/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hospotho/Image-Viewer/f2845cc62574ce0f1de061e4879ecb680392a365/icon/icon128.png -------------------------------------------------------------------------------- /icon/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hospotho/Image-Viewer/f2845cc62574ce0f1de061e4879ecb680392a365/icon/icon16.png -------------------------------------------------------------------------------- /icon/ko-fi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hospotho/Image-Viewer/f2845cc62574ce0f1de061e4879ecb680392a365/icon/ko-fi.png -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tony Ho 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 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_app_name__", 3 | "description": "__MSG_app_desc__", 4 | "version": "1.41", 5 | "default_locale": "en", 6 | "manifest_version": 3, 7 | "icons": { 8 | "16": "/icon/icon16.png", 9 | "128": "/icon/icon128.png" 10 | }, 11 | "permissions": ["storage", "contextMenus", "scripting", "webNavigation"], 12 | "host_permissions": ["*://*/*", "file:///*"], 13 | "background": { 14 | "scripts": ["background.js"], 15 | "service_worker": "background.js" 16 | }, 17 | "options_page": "/page/options.html", 18 | "action": { 19 | "default_icon": { 20 | "16": "/icon/icon16.png", 21 | "128": "/icon/icon128.png" 22 | } 23 | }, 24 | "content_scripts": [ 25 | { 26 | "js": ["/scripts/activate-url.js"], 27 | "matches": ["*://*/*", "file:///*"], 28 | "all_frames": true 29 | }, 30 | { 31 | "world": "MAIN", 32 | "js": ["/scripts/hook.js"], 33 | "matches": ["*://*/*"], 34 | "run_at": "document_start", 35 | "all_frames": true 36 | } 37 | ], 38 | "commands": { 39 | "open-image-viewer": { 40 | "suggested_key": { 41 | "default": "Alt+1" 42 | }, 43 | "description": "__MSG_view_images_in_image_viewer__" 44 | }, 45 | "open-image-viewer-without-size-filter": { 46 | "suggested_key": { 47 | "default": "Alt+Shift+1" 48 | }, 49 | "description": "__MSG_view_all_images_in_image_viewer__" 50 | }, 51 | "open-image-viewer-in-canvases-mode": { 52 | "suggested_key": { 53 | "default": "Alt+2" 54 | }, 55 | "description": "__MSG_view_canvas_in_image_viewer__" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "@types/chrome": "^0.0.266", 4 | "eslint": "^8.57.0", 5 | "eslint-config-standard": "^17.1.0", 6 | "eslint-plugin-import": "^2.29.1", 7 | "eslint-plugin-n": "^16.6.2", 8 | "eslint-plugin-promise": "^6.6.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /page/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Options - Image Viewer 7 | 8 | 9 | 10 | 11 | 12 |

Image Viewer Options

13 | 14 | 40 | 41 |
42 |
43 | 83 |
84 |
85 | Image size filter 86 | 100 |
101 |
102 | Speed control 103 | 137 |
138 |
139 | Image search hotkey 140 | 182 |
183 |
184 | Function hotkey 185 | 195 |
196 |
197 | Domain specific settings 198 | 237 |
238 |
239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /page/options.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict' 3 | 4 | const zoom = document.querySelector('input#zoomRatio') 5 | const rotate = document.querySelector('input#rotateDeg') 6 | 7 | const width = document.querySelector('input#minWidth') 8 | const height = document.querySelector('input#minHeight') 9 | const svgFilter = document.querySelector('input#svgFilter') 10 | 11 | const debounce = document.querySelector('input#debouncePeriod') 12 | const throttle = document.querySelector('input#throttlePeriod') 13 | const auto = document.querySelector('input#autoPeriod') 14 | 15 | const google = document.querySelector('input#googleSearch') 16 | const yandex = document.querySelector('input#yandexSearch') 17 | const sauceNAO = document.querySelector('input#sauceNAOSearch') 18 | const ascii2d = document.querySelector('input#ascii2dSearch') 19 | const useAll = document.querySelector('input#useAllSearch') 20 | 21 | const scrollHotkey = document.querySelector('input#scrollHotkey') 22 | const downloadHotkey = document.querySelector('input#downloadHotkey') 23 | 24 | const hoverCheck = document.querySelector('textarea#hoverCheckDisableList') 25 | const autoScroll = document.querySelector('textarea#autoScrollEnableList') 26 | const imageUnlazy = document.querySelector('textarea#imageUnlazyDisableList') 27 | 28 | const defaultOptions = { 29 | fitMode: 'both', 30 | zoomRatio: 1.2, 31 | rotateDeg: 15, 32 | minWidth: 180, 33 | minHeight: 150, 34 | svgFilter: true, 35 | debouncePeriod: 1500, 36 | throttlePeriod: 80, 37 | autoPeriod: 2000, 38 | searchHotkey: ['Shift + Q', 'Shift + W', 'Shift + A', 'Shift + S', 'Ctrl + Shift + Q', ''], 39 | customUrl: ['https://example.com/search?query={imgSrc}&option=example_option'], 40 | functionHotkey: ['Shift + R', 'Shift + D'], 41 | hoverCheckDisableList: [], 42 | autoScrollEnableList: ['x.com', 'www.instagram.com', 'www.facebook.com'], 43 | imageUnlazyDisableList: [] 44 | } 45 | 46 | //==========utility========== 47 | function i18n() { 48 | chrome.i18n.getAcceptLanguages(languages => { 49 | const exist = ['en', 'ja', 'zh_CN', 'zh_TW'] 50 | let displayLanguages = 'en' 51 | for (const lang of languages) { 52 | if (exist.includes(lang.replace('-', '_'))) { 53 | displayLanguages = lang 54 | break 55 | } 56 | if (exist.includes(lang.slice(0, 2))) { 57 | displayLanguages = lang.slice(0, 2) 58 | break 59 | } 60 | } 61 | document.documentElement.setAttribute('lang', displayLanguages) 62 | }) 63 | 64 | for (const el of document.querySelectorAll('[data-i18n]')) { 65 | const tag = el.getAttribute('data-i18n') 66 | const message = chrome.i18n.getMessage(tag) 67 | if (!message) continue 68 | el.innerHTML = message 69 | if (el.value !== '') el.value = message 70 | } 71 | } 72 | 73 | function resetDefaultOptions() { 74 | chrome.storage.sync.set({options: defaultOptions}, () => { 75 | alert('Options have been reset to default.') 76 | console.log(defaultOptions) 77 | }) 78 | } 79 | 80 | function keyToString(e) { 81 | let result = '' 82 | if (e.ctrlKey) result += 'Ctrl + ' 83 | if (e.altKey) result += 'Alt + ' 84 | if (e.shiftKey) result += 'Shift + ' 85 | if (result !== '' && /^[\S]{1}$/.test(e.key)) { 86 | result += e.key.toUpperCase() 87 | } 88 | return result 89 | } 90 | 91 | function addNewCustom() { 92 | const custom = document.querySelectorAll('input.customSearchUrl') 93 | const last = custom[custom.length - 1] 94 | const li = last.parentNode 95 | 96 | const i18n = [chrome.i18n.getMessage('custom_search'), chrome.i18n.getMessage('custom_search_url')] 97 | const htmlStr = 98 | `
  • ` + 99 | `
  • ` 100 | li.insertAdjacentHTML('afterend', htmlStr) 101 | 102 | for (const input of document.querySelectorAll('input.hotkey')) { 103 | input.addEventListener('keydown', e => { 104 | e.preventDefault() 105 | input.value = keyToString(e) 106 | }) 107 | } 108 | } 109 | 110 | function setValue(options) { 111 | try { 112 | document.querySelector(`input#fit-${options.fitMode}`).checked = true 113 | zoom.value = options.zoomRatio 114 | zoom.nextElementSibling.textContent = options.zoomRatio 115 | rotate.value = options.rotateDeg 116 | rotate.nextElementSibling.textContent = options.rotateDeg 117 | 118 | width.value = options.minWidth 119 | height.value = options.minHeight 120 | svgFilter.checked = options.svgFilter 121 | 122 | debounce.value = options.debouncePeriod 123 | throttle.value = options.throttlePeriod 124 | auto.value = options.autoPeriod 125 | 126 | google.value = options.searchHotkey[0] 127 | yandex.value = options.searchHotkey[1] 128 | sauceNAO.value = options.searchHotkey[2] 129 | ascii2d.value = options.searchHotkey[3] 130 | useAll.value = options.searchHotkey[4] 131 | 132 | for (let i = 6; i < options.searchHotkey.length; i++) { 133 | addNewCustom() 134 | } 135 | const custom = document.querySelectorAll('input.customSearch') 136 | const customUrl = document.querySelectorAll('input.customSearchUrl') 137 | for (let i = 0; i < custom.length; i++) { 138 | if (i < options.searchHotkey.length - 5) { 139 | custom[i].value = options.searchHotkey[i + 5] 140 | customUrl[i].value = options.customUrl[i] 141 | } else { 142 | custom[i].value = '' 143 | customUrl[i].value = '' 144 | } 145 | } 146 | 147 | scrollHotkey.value = options.functionHotkey[0] 148 | downloadHotkey.value = options.functionHotkey[1] 149 | 150 | hoverCheck.value = options.hoverCheckDisableList.join('\n') 151 | autoScroll.value = options.autoScrollEnableList.join('\n') 152 | imageUnlazy.value = options.imageUnlazyDisableList.join('\n') 153 | } catch (e) { 154 | resetDefaultOptions() 155 | setValue(defaultOptions) 156 | alert('Failed to use existing options') 157 | } 158 | } 159 | 160 | //==========main========== 161 | function initFormEvent() { 162 | zoom.addEventListener('input', () => { 163 | document.querySelector('span#zoomDisplay').textContent = zoom.value 164 | }) 165 | 166 | rotate.addEventListener('input', () => { 167 | const span = document.querySelector('span#rotateDisplay') 168 | span.textContent = rotate.value 169 | span.nextElementSibling.style = 360 % rotate.value !== 0 ? 'display: inline' : '' 170 | }) 171 | 172 | const debounceDesc = document.querySelector('li#debounceDesc') 173 | debounce.addEventListener('focus', () => (debounceDesc.style = 'display: block; padding: 0px 0px 10px 10px;')) 174 | debounce.addEventListener('focusout', () => (debounceDesc.style = '')) 175 | 176 | const throttleDesc = document.querySelector('li#throttleDesc') 177 | throttle.addEventListener('focus', () => (throttleDesc.style = 'display: block; padding: 0px 0px 10px 10px;')) 178 | throttle.addEventListener('focusout', () => (throttleDesc.style = '')) 179 | 180 | const autoDesc = document.querySelector('li#autoDesc') 181 | auto.addEventListener('focus', () => (autoDesc.style = 'display: block; padding: 0px 0px 10px 10px;')) 182 | auto.addEventListener('focusout', () => (autoDesc.style = '')) 183 | 184 | for (const input of document.querySelectorAll('input.hotkey')) { 185 | input.addEventListener('keydown', e => { 186 | e.preventDefault() 187 | input.value = keyToString(e) 188 | }) 189 | } 190 | 191 | document.querySelector('button#addNewCustom').addEventListener('click', addNewCustom) 192 | } 193 | 194 | function initFormButton() { 195 | document.querySelector('button#save').addEventListener('click', () => { 196 | const options = {} 197 | options.fitMode = document.querySelector('input[name="fit"]:checked').value 198 | options.zoomRatio = Number(zoom.value) 199 | options.rotateDeg = Number(rotate.value) 200 | options.minWidth = Number(width.value) 201 | options.minHeight = Number(height.value) 202 | options.svgFilter = svgFilter.checked 203 | options.debouncePeriod = Number(debounce.value) 204 | options.throttlePeriod = Number(throttle.value) 205 | options.autoPeriod = Number(auto.value) 206 | 207 | const hotkeyList = [google.value, yandex.value, sauceNAO.value, ascii2d.value, useAll.value] 208 | const customUrlList = [] 209 | const custom = document.querySelectorAll('input.customSearch') 210 | const customUrl = document.querySelectorAll('input.customSearchUrl') 211 | for (let i = 0; i < custom.length; i++) { 212 | if (!custom[i].value && !customUrl[i].value) continue 213 | hotkeyList.push(custom[i].value) 214 | customUrlList.push(customUrl[i].value) 215 | } 216 | options.searchHotkey = hotkeyList 217 | options.customUrl = customUrlList 218 | 219 | options.functionHotkey = [scrollHotkey.value, downloadHotkey.value] 220 | 221 | const hoverCheckDisableList = hoverCheck.value.split('\n') 222 | const autoScrollEnableList = autoScroll.value.split('\n') 223 | const imageUnlazyDisableList = imageUnlazy.value.split('\n') 224 | options.hoverCheckDisableList = hoverCheckDisableList 225 | options.autoScrollEnableList = autoScrollEnableList 226 | options.imageUnlazyDisableList = imageUnlazyDisableList 227 | 228 | chrome.storage.sync.set({options: options}, () => { 229 | if (chrome.runtime?.id) chrome.runtime.sendMessage('update_options') 230 | console.log(options) 231 | alert('Options have been saved.') 232 | }) 233 | }) 234 | 235 | document.querySelector('button#reset').addEventListener('click', () => { 236 | setValue(defaultOptions) 237 | resetDefaultOptions() 238 | }) 239 | } 240 | 241 | function checkUpdate(options) { 242 | const newOptionList = Object.keys(defaultOptions).filter(key => key in options === false) 243 | if (newOptionList.length === 0) return 244 | const message = newOptionList 245 | .map(key => key.replace(/([A-Z])/g, '_$1').toLowerCase()) 246 | .map(tag => chrome.i18n.getMessage(tag) || tag) 247 | .join('\n') 248 | alert(chrome.i18n.getMessage('new_option') + ':\n' + message) 249 | 250 | // sync with default options 251 | for (const key of newOptionList) { 252 | options[key] = defaultOptions[key] 253 | } 254 | chrome.storage.sync.set({options: options}, () => { 255 | if (chrome.runtime?.id) chrome.runtime.sendMessage('update_options') 256 | console.log(options) 257 | }) 258 | } 259 | 260 | async function init() { 261 | i18n() 262 | const {options} = await chrome.storage.sync.get('options') 263 | checkUpdate(options) 264 | setValue(options) 265 | initFormEvent() 266 | initFormButton() 267 | } 268 | 269 | init() 270 | })() 271 | -------------------------------------------------------------------------------- /page/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Release Notes - Image Viewer 7 | 8 | 9 | 10 | 11 | 12 |

    Image Viewer Release notes

    13 | 14 | 28 |
    29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /page/popup.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict' 3 | 4 | const rawText = ` 5 | 1.41 [2025-02-06]: 6 | Stability update 7 | This version was originally planned as a major update, but the development of new features was delayed 8 | 1. Added a hotkey command for canvas mode 9 | 2. Added support for background images in pseudo elements 10 | 3. Improved scroll unlazy logic 11 | 4. Improved right click image size referencing logic 12 | 5. Other bug fixes and improvements 13 | P.S. Starting with this version, the extension will also be published on addons.mozilla.org for Firefox users 14 | 15 | 1.40 [2024-10-14]: 16 | 1. Added a new option to allow users to disable image unlazy on specific domains 17 | 2. Introduced a web demo, enabling users to try the feature before installation 18 | 3. Updated the options page and added a simple support page 19 | 4. Fixed a bug in the new view canvas feature that caused issues on sites like Notion 20 | 21 | 1.39.1 [2024-10-01]: 22 | Patch Update 23 | 1. Fixed a bug in the new view canvas features that caused issues with some sites like Google Sheets 24 | 25 | 1.39 [2024-09-29]: 26 | Major Update 27 | 1. Added an action to the icon context menu allowing users to view canvas elements 28 | // Note: This feature only supports snapshots, not GIF creation 29 | // May also be useful for cases where an image is visible but not accessible in normal mode 30 | // This could include an image drawn on a canvas element 31 | 2. Added support for local and blob images to mainstream reverse search 32 | 3. Fixed navigation, it will now correctly wait for images to be rendered on the screen 33 | 4. The space bar can now be used to send a middle click to an image (previously only "0" could be used) 34 | 5. Other bug fixes and improvements 35 | 36 | 1.38 [2024-09-09]: 37 | Stability update 38 | 1. Added support for data URL images to mainstream reverse search 39 | 2. Fixed a bug that could change the website's default layout 40 | 3. Fixed a bug that could toggle the website's default hotkeys (eg. page navigation) 41 | 4. Other bug fixes and improvements 42 | 43 | 1.37 [2024-08-11]: 44 | Performance and Stability update 45 | 1. The control panel will now auto hide after 1.5 seconds of mouse hover 46 | // move cursor over buttons will toggle the panel again 47 | // provides clearer view when using scroll to view image 48 | 2. Improved image viewer's logic for build/update image list 49 | 3. Refactored image collection logic to enhance stability of the image list 50 | 4. Rewritten auto scroll logic to ensure no images are skipped 51 | 5. Enhanced code quality 52 | 6. Other bug fixes and improvements 53 | 54 | 1.36 [2024-07-22]: 55 | Major Update 56 | 1. Added a hotkey for auto navigation (shift + arrow keys) 57 | 2. Added ton of code to support of custom element 58 | 3. Add sub-image check to improve image unlazy in url mode 59 | 4. Improve and refactor iframe image extraction logic 60 | 5. Improve CSS and layout of the image viewer 61 | 6. Refactor data structure for image info 62 | 7. Other bug fixes and improvements 63 | 64 | 1.35 [2024-07-03]: 65 | 1. Reduced zoom & rotate transition flash 66 | 2. Improved auto update logic 67 | 3. Enhanced ability to find larger size raw images 68 | 4. Reworked unlazy logic, no longer need to wait when reopening within a short time 69 | 5. Reworked iframe logic, can now handle iframe in iframe cases 70 | 6. Fixed a bug that changed current index after image list update 71 | 7. Other bug fixes and improvements 72 | 73 | 1.34 [2024-04-11]: 74 | Stability update 75 | 1. Prevented image loading flash in URL mode 76 | 2. Add smooth transition for image transform 77 | 3. Fixed a bug where AltGraph could not be used with Ctrl in hotkey combinations 78 | // related hotkey: image transformation and image reverse search 79 | 4. Improved code performance 80 | 5. Implemented error handling to minimize minor errors displayed to users 81 | 6. Added support for new type of unlazy (simulate mouse hover) 82 | 7. Other bug fixes and improvements 83 | 84 | 1.33 [2024-01-15]: 85 | Functional Update 86 | 1. Added a new default fit mode option: "Original size (does not exceed window)" 87 | 2. Added a maximum size limit (3x) for other fit modes to prevent enlarging small images too much 88 | 3. Added a new hotkey (Shift + B) for switching the background color: transparent -> black -> white 89 | 4. Added new hotkeys for image transformation: 90 | // Move: Ctrl + Alt + ↑↓←→ / WASD 91 | // Zoom: Alt + ↑↓ / WS 92 | // Rotate: Alt + ←→ / AD 93 | 5. Improved auto-scrolling 94 | 6. Added support for more edge cases 95 | 7. Other bug fixes and improvements 96 | 97 | 1.32 [2023-12-31]: 98 | 1. Improved accuracy of image middle click redirect 99 | 2. Enhanced size filter referencing of picking an image by right click 100 | 3. Solve CSS issues related to lazy images on some websites 101 | 4. Added support for the embed element 102 | 5. Refactored and improved code logic 103 | 6. Other bug fixes and improvements 104 | 105 | 1.31 [2023-10-22]: 106 | 1. Rotation now rotates around the center of the viewpoint 107 | 2. Auto scroll hotkey will toggle auto scroll instead of just starting it 108 | 3. Navigation with "WASD" is now supported 109 | 4. Support fast navigation by pressing the Ctrl key at the same time to activate it 110 | 5. Support memory of last image when restarting in page mode 111 | 6. Enhanced code quality 112 | 7. Other bug fixes and improvements 113 | 114 | 1.30 [2023-08-27]: 115 | Stability update 116 | 1. Improved SVG filtering 117 | 2. Added support for multiple layers unlazy 118 | 3. Enhanced logic for getting raw image URLs 119 | 4. Added support for more edge cases 120 | 5. Other bug fixes and improvements 121 | 122 | 1.29 [2023-08-08]: 123 | 1. Corrected code related to the service worker lifecycle 124 | 2. Enhanced unlazy logic to handle additional cases 125 | 3. Improved logic for updating the size filter when there are images of the same kind as the picked image 126 | 4. Enhanced the user experience on auto scroll 127 | 5. Numerous bug fixes and minor improvements 128 | 129 | 1.28 [2023-07-10]: 130 | Major Update 131 | 1. Added a hotkey to manually enable auto scroll 132 | 2. Added a hotkey to download images collected by image viewer 133 | // Note: This extension is not a resource downloader 134 | // Download functionality is limited to basic features 135 | // eg. selecting a download range and packaging in a zip file 136 | 3. Improved first display time of image viewer 137 | 4. Improved middle-click redirect to open the original image's hyperlink 138 | 5. Improved correctness of right click image pickup 139 | 6. Other bug fixes and improvements 140 | 141 | 1.27 [2023-07-06]: 142 | 1. Improved image selection, decrease the priority of image placeholder and image sprite 143 | 2. Improved border display after using moveTo 144 | 3. Improved auto scrolling and auto update 145 | 4. Some bug fixes 146 | 147 | 1.26 [2023-06-17]: 148 | 1. Improved the logic of using middle click to open the link of current image 149 | 2. Fixed a bug that caused jumping in viewer index 150 | 3. Fixed a bug that prevented the image viewer from automatically starting for image URLs 151 | 4. Other bug fixes and improvements 152 | 153 | 1.25 [2023-06-04]: 154 | 1. AltGraph key now functions the same as Alt key in hotkey 155 | 2. More intuitive zoom, where zooming now occurs at the screen center instead of the image center 156 | 3. Fixed the incorrect position of the border display after the moveTo operation 157 | 4. Fixed a bug that caused a conflict in the scroll function 158 | 5. Added a check for iframes to handle a bug in Chrome 159 | 6. Removed code that caused extra rendering time 160 | 7. Added caching to enhance performance 161 | 8. Improved performance on right click image pickup 162 | 163 | 1.24 [2023-06-01]: 164 | 1. Introduces method for old style lazy image 165 | 2. Enhance the moveTo function and label border 166 | 3. Improve stability of the extension 167 | 4. Fix bugs and improve performance 168 | 169 | 1.23 [2023-05-29]: 170 | 1. Add temporary image list storage 171 | 2. Significantly reduced startup time by approximately 3-10 times 172 | 3. Refine UI 173 | 4. Improve code logic 174 | 175 | 1.22 [2023-05-28]: 176 | 1. Support deeper-layer iframes 177 | 2. Enhance the moveTo function 178 | 3. Revamp border display following moveTo 179 | 4. Support additional edge cases 180 | 5. Improve performance and fix bugs 181 | 182 | 1.21 [2023-05-27]: 183 | 1. Fixed a bug when getting the image list, so it won't repeat the same image with different sizes 184 | 2. Fixed the "moveTo" button, now it functions correctly on websites like Instagram and Twitter 185 | 3. Fixed the image update, so it won't jump back to the first image when updating 186 | 4. Fixed a bug related to image looping, now it will wait for an image update when it reaches the end 187 | 188 | 1.20 [2023-05-26]: 189 | 1. Improved auto update and auto scroll 190 | 2. More stability on image file URLs 191 | 3. Added support for more iframe images 192 | 4. Improved performance and fixed bugs 193 | 194 | 1.19 [2023-05-14]: 195 | 1. Improve the stability of auto scroll 196 | 2. Improve the code logic for better performance 197 | 3. Fix a lot of bugs 198 | 199 | 1.18 [2023-05-04]: 200 | 1. Support auto scroll 201 | 2. Add options to enable auto scroll and disable hover check 202 | 3. Refactor code for better readability 203 | 4. Fix bug related to hover check and other minor bugs 204 | 205 | 1.17 [2023-04-30]: 206 | Stability update 207 | 1. Add some code to increase the stability 208 | 2. Add handle to more edge cases 209 | 3. Fix bugs 210 | 211 | 1.16 [2023-04-10]: 212 | 1. Image viewer now collects images after website adding new content 213 | // usually website update is toggled by scroll to the end of the page 214 | // you can archive it by scrolling on the scrollbar or press "End" key on keyboard 215 | // you may also use other "next page" script/extension 216 | 2. Fix issues for youtube thumbnail 217 | 3. Fix bugs related to last update 218 | 4. Refactor code to improving program structure 219 | 220 | 1.15 [2023-04-05]: 221 | Large Update 222 | 1. Add support on update image in the viewer 223 | 2. Solve the problem for image viewer can't be open on some websites 224 | 3. Fix CORS issues for iframe images 225 | 4. Fix other issues in rare situations 226 | 5. Improve performance and fix some bugs 227 | 228 | 1.14 [2023-04-01]: 229 | 1. Improve CSS of image viewer 230 | 2. Improve performance of right click image pickup 231 | 3. Add an icon image pre-check before unlazy image to improve performance 232 | 4. Enhance the method of getting image wrapper size 233 | 5. Bug fixes 234 | 235 | 1.13 [2023-03-18]: 236 | 1. Improve right click image pickup performance 237 | 2. Improve stability on image unlazy 238 | 3. Extend the loading time limit for images inside image viewer 239 | 4. Fix lot of typos and bugs 240 | 241 | 1.12 [2023-02-14]: 242 | 1. Add this popup page to show release notes when install or update 243 | 2. Improve stability 244 | 3. Add domain white list for image unlazy 245 | // create issues on github if you want to add domain to the list 246 | // may move to option page or just hide in source code 247 | 248 | 1.11 [2023-02-11]: 249 | 1. Images are now order by its real location 250 | 3. No longer use dataURL, ObjectURL is faster and better for the browser to render images 251 | 2. Min size filter will also considers wrapper of the selected image 252 | 4. Some website that disabled right click menu. Add "view last right click" in icon menu to handle it 253 | 254 | 1.10 [2023-02-11]: 255 | 1. Add MoveTo support for iframe images 256 | 2. Improve right click image pickup 257 | 3. Improve image check size method 258 | 259 | 1.9 [2023-01-13]: 260 | 1. Support image pickup using right click 261 | 2. Delay execution of worker script to improve performance 262 | 263 | 1.8 [2022-10-30]: 264 | 1. Improve the support of viewing images inside iframe 265 | 2. Refactor code to tidy up code related to iframe 266 | 267 | 1.7 [2022-10-04]: 268 | 1. Improve support on iframe images 269 | 2. Improve simpleUnlazyImage() 270 | 3. Add more keyboard shortcuts and svg filter in option 271 | 272 | 1.6 [2022-09-03]: 273 | 1. Support images inside iframe 274 | 2. Improve data transfer between content script and background 275 | 276 | 1.5 [2022-08-22]: 277 | 1. Renew simpleUnlazyImage() 278 | 2. Improve image-viewer.js 279 | 3. Support hotkey for reverse search image 280 | 281 | 1.4 [2022-08-10]: 282 | 1. Improve simpleUnlazyImage() 283 | 2. Support video element 284 | 3. Improve MoveTo button logic 285 | 4. Prevent input leak out from image viewer 286 | 5. Improve simpleUnlazyImage() 287 | 6. Add utility.js to separate utility function 288 | 289 | 1.3 [2022-07-01]: 290 | 1. Delay loading of image-viewer.js to improve performance 291 | 2. Add command support 292 | 3. Improve image unlazy 293 | 4. Renew activate image method to increase readability 294 | 295 | 1.2 [2022-07-01]: 296 | 1. Add simpleUnlazyImage() to unlazy image before getting image list 297 | 2. Change CSS to pin image viewer counter 298 | 299 | 1.1 [2022-07-01]: 300 | 1. Support mirror effect 301 | 2. Replace old transform method with matrix to improve performance 302 | 303 | 1.0 [2022-06-29]: 304 | First release on github 305 | ` 306 | function createNotes() { 307 | const data = rawText.split('\n\n').map(t => t.trim().split('\n')) 308 | 309 | const noteContainerGroup = document.createElement('div') 310 | noteContainerGroup.classList.add('note-container-group') 311 | for (const textList of data) { 312 | const noteContainer = document.createElement('div') 313 | noteContainer.classList.add('note-container') 314 | 315 | const bar = document.createElement('button') 316 | bar.classList.add('bar') 317 | bar.type = 'button' 318 | bar.textContent = textList.shift() 319 | 320 | const noteText = document.createElement('div') 321 | noteText.classList.add('noteText') 322 | for (const line of textList) { 323 | const p = document.createElement('p') 324 | p.textContent = line 325 | noteText.appendChild(p) 326 | } 327 | 328 | bar.onclick = () => { 329 | if (noteContainer.classList.contains('active')) { 330 | noteContainer.classList.remove('active') 331 | noteText.style.maxHeight = null 332 | } else { 333 | noteContainer.classList.add('active') 334 | noteText.style.maxHeight = noteText.scrollHeight + 'px' 335 | } 336 | } 337 | 338 | noteContainer.appendChild(bar) 339 | noteContainer.appendChild(noteText) 340 | noteContainerGroup.appendChild(noteContainer) 341 | } 342 | document.body.appendChild(noteContainerGroup) 343 | } 344 | 345 | function toggleFirstNote() { 346 | const firstNote = document.querySelector('div.note-container-group > div:nth-child(1) > button') 347 | firstNote.nextElementSibling.style.transitionDuration = '0s' 348 | firstNote.click() 349 | setTimeout(() => (firstNote.nextElementSibling.style.transitionDuration = ''), 100) 350 | } 351 | 352 | function i18n() { 353 | chrome.i18n.getAcceptLanguages(languages => { 354 | const exist = ['en', 'ja', 'zh_CN', 'zh_TW'] 355 | let displayLanguages = 'en' 356 | for (const lang of languages) { 357 | if (exist.includes(lang.replace('-', '_'))) { 358 | displayLanguages = lang 359 | break 360 | } 361 | if (exist.includes(lang.slice(0, 2))) { 362 | displayLanguages = lang.slice(0, 2) 363 | break 364 | } 365 | } 366 | document.documentElement.setAttribute('lang', displayLanguages) 367 | }) 368 | 369 | for (const el of document.querySelectorAll('[data-i18n]')) { 370 | const tag = el.getAttribute('data-i18n') 371 | const message = chrome.i18n.getMessage(tag) 372 | if (!message) continue 373 | el.textContent = message 374 | if (el.value !== '') el.value = message 375 | } 376 | } 377 | 378 | function init() { 379 | createNotes() 380 | toggleFirstNote() 381 | i18n() 382 | } 383 | 384 | init() 385 | })() 386 | -------------------------------------------------------------------------------- /page/support.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Support - Image Viewer 7 | 8 | 9 | 10 | 11 |

    Image Viewer Support

    12 |
    13 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /page/support.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict' 3 | 4 | chrome.i18n.getAcceptLanguages(languages => { 5 | const exist = ['en', 'ja', 'zh_CN', 'zh_TW'] 6 | let displayLanguages = 'en' 7 | for (const lang of languages) { 8 | if (exist.includes(lang.replace('-', '_'))) { 9 | displayLanguages = lang 10 | break 11 | } 12 | if (exist.includes(lang.slice(0, 2))) { 13 | displayLanguages = lang.slice(0, 2) 14 | break 15 | } 16 | } 17 | document.documentElement.setAttribute('lang', displayLanguages) 18 | }) 19 | 20 | for (const el of document.querySelectorAll('[data-i18n]')) { 21 | const tag = el.getAttribute('data-i18n') 22 | const message = chrome.i18n.getMessage(tag) 23 | if (!message) continue 24 | el.textContent = message 25 | if (el.value !== '') el.value = message 26 | } 27 | })() 28 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Image Viewer 2 | 3 |

    4 |

    Image Viewer is a manifest V3 Chrome extension that improves your image viewing experience.

    5 | 6 | If you like this extension, you can [buy me a coffee](https://ko-fi.com/tonymilktea) 7 | 8 | ## Features 9 | 10 | 1. Collect and view all images on the page. 11 | 2. Support video posters, canvas element and images in iframes. 12 | 3. Auto replace lazy loaded or resized images with original image. 13 | 4. Redirect middle click to original image to open link you want. 14 | 5. Go to original image on the page. 15 | 6. Fit, zoom, rotate and mirror the image. 16 | 7. Hotkey for image reverse search. 17 | 8. Download collected images. 18 | 9. Easy to use. 19 | 10. And more... 20 | 21 | ## Installation 22 | 23 | [Web Demo](https://hospotho.github.io/Image-Viewer/) (Does not include some extension-only features) 24 | 25 | You can install release version from [Chrome Web Store](https://chrome.google.com/webstore/detail/image-viewer/ghdcoodfcolpdebbdhbgkbodbjololfl) or [Firefox Add-ons](https://addons.mozilla.org/firefox/addon/syrup-image-viewer/) development version follow steps below: 26 | 27 | 1. Download the source code and place it anywhere you want. 28 | 2. Open your browser and go to `chrome://extensions`. 29 | 3. Enable Developer Mode. 30 | 4. Click the "Load Unpacked" button and select the folder with the source code. 31 | 32 | Note: Any tabs opened before the installation require a reload. 33 | 34 | ## How to use 35 | 36 | After adding this extension to your browser, it is recommended to pin it to the toolbar. 37 | 38 | For image tabs, Image Viewer will be activate automatically. 39 | 40 | For normal websites, you can activate Image Viewer by choose this extension from the right-click menu, click its icon on the toolbar or use keyboard hotkey (default Alt+1). 41 | 42 | For additional options, right-click the extension icon on the toolbar. You can start the Image Viewer with disabled size filter or start with the last picked image (use it when the right-click menu is disabled by the website). 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 |
    ActionControls
    Pick image
    (size filter will use this image as reference)
    right-click on the image
    View previous/next imagewasd
    Scroll on the control bar
    Scroll on the close button
    Fast navigation
    (10 images, no throttle)
    Ctrl+
    Auto navigation / Slideshow
    (until end or user interrupt)
    Shift+
    Go to original image on pageEnter
    click "Move To" button on the control bar
    middle-click the original image
    (Open tab for post, video, etc.)
    middle-click on the image
    space or 0 (both number row and numeric keypad)
    Fitting imageClick fitting buttons on the control bar
    Move imageclick and drag
    Ctrl+Alt+wasd
    Reset imagedouble-click anywhere
    Zoom imageScroll on the image
    Alt+ws
    Rotate imageHold Alt and scroll
    Alt+ad
    Mirror imageHold Alt and click
    Change background color
    (loop: transparent -> black -> white)
    Shift+b
    Download current imageCtrl+Shift+d
    Image reverse searchPress the hotkeys defined in setting
    Download collected imagesShift+d (default)
    Enable auto scrollShift+r (default)
    Close Image ViewerESC or NumpadAdd
    Click the close button
    Close current tabright-click the close button
    150 | 151 | ## Browser support 152 | 153 | The entire project was written in Vanilla JavaScript with extension API supported by Chromium-based browsers. It should also work on Firefox, but it has not been tested yet. 154 | 155 | The standalone `image-viewer.js` should work on all modern browsers, and you can integrated into your own website with own collect image script. 156 | 157 | You may also use `image-viewer.js` with your own script with Tampermonkey or other alternatives. 158 | 159 | ## ToDo 160 | 161 | 1. `image-viewer.min.js` 162 | 2. support more image display mode 163 | 164 | ## History 165 | 166 | The prototype of this project was created by Eky Kwan under the MIT License, and the author of the translations in `_locales` is unknown. The first release v0.1 was launched on 2012-07-05, and the last release v0.1.6 was on 2012-08-12. However, the license file was either lost or not included in the Chrome Web Store version. 167 | 168 | Since I started using this extension, many new features have been added to the project. You can find the oldest version [here](https://github.com/hospotho/Image-Viewer-Legacy), some mirroring websites may still have the raw version of v0.1.6. 169 | 170 | The old version was hard to extend, and I felt tired of it in June 2022. Therefore, I decided to clean up all the old-style, messy jQuery code and rewrite the project completely. The rewrite is now complete and has also been upgraded to manifest V3. 171 | 172 | This project is currently developed and maintained by me. 173 | 174 | ## License 175 | 176 | MIT license -------------------------------------------------------------------------------- /scripts/action-canvas.js: -------------------------------------------------------------------------------- 1 | ;(async function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | if (typeof ImageViewerUtils !== 'object') { 11 | await safeSendMessage('load_utility') 12 | } 13 | 14 | if (document.body.classList.contains('iv-attached')) { 15 | ImageViewer('close_image_viewer') 16 | return 17 | } 18 | 19 | // init 20 | const options = window.ImageViewerOption 21 | options.closeButton = true 22 | options.canvasMode = true 23 | window.ImageViewerLastDom = null 24 | 25 | const orderedCanvasList = await ImageViewerUtils.getOrderedCanvasList(options) 26 | if (orderedCanvasList.length === 0) { 27 | alert('No canvas found') 28 | return 29 | } 30 | 31 | // build image viewer 32 | ImageViewer(orderedCanvasList, options) 33 | })() 34 | -------------------------------------------------------------------------------- /scripts/action-folder.js: -------------------------------------------------------------------------------- 1 | ;(async function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | if (typeof ImageViewerUtils !== 'object') { 11 | await safeSendMessage('load_utility') 12 | } 13 | 14 | if (document.body.classList.contains('iv-attached')) { 15 | ImageViewer('close_image_viewer') 16 | return 17 | } 18 | 19 | // init 20 | const options = window.ImageViewerOption 21 | options.closeButton = true 22 | 23 | const anchorList = [...document.getElementsByTagName('a')].filter(a => !a.href.endsWith('/')) 24 | const isImageList = await safeSendMessage({msg: 'is_file_image', urlList: anchorList.map(a => a.href)}) 25 | const sizeList = await Promise.all(anchorList.map((a, i) => isImageList[i] && ImageViewerUtils.getImageRealSize(a.href))) 26 | 27 | const imageDataList = [] 28 | const minSize = Math.min(options.minWidth, options.minHeight) 29 | for (let i = 0; i < anchorList.length; i++) { 30 | if (sizeList[i] >= minSize) imageDataList.push({src: anchorList[i].href, dom: anchorList[i]}) 31 | } 32 | 33 | // build image viewer 34 | ImageViewer(imageDataList, options) 35 | })() 36 | -------------------------------------------------------------------------------- /scripts/action-image.js: -------------------------------------------------------------------------------- 1 | ;(async function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | if (typeof ImageViewerUtils !== 'object') { 11 | await safeSendMessage('load_utility') 12 | } 13 | 14 | if (document.body.classList.contains('iv-attached')) return 15 | 16 | // init 17 | const options = window.ImageViewerOption 18 | options.closeButton = true 19 | 20 | // update image size filter 21 | const nodeInfo = await safeSendMessage('get_info') 22 | const [srcUrl, nodeSize] = nodeInfo 23 | if (nodeSize) { 24 | options.minWidth = Math.min(nodeSize, options.minWidth) 25 | options.minHeight = Math.min(nodeSize, options.minHeight) 26 | } 27 | 28 | for (let i = 0; i < 10; i++) { 29 | if (window.ImageViewerLastDom !== undefined) break 30 | await new Promise(resolve => setTimeout(resolve, 20)) 31 | } 32 | const dom = window.ImageViewerLastDom 33 | const domRect = dom?.getBoundingClientRect() 34 | const domSize = domRect ? [domRect.width, domRect.height] : [0, 0] 35 | ImageViewerUtils.updateWrapperSize(dom, domSize, options) 36 | 37 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options) 38 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList) 39 | window.backupImageList = Array.from(combinedImageList) 40 | 41 | // find image index 42 | options.index = ImageViewerUtils.searchImageInfoIndex({src: srcUrl, dom: dom}, window.backupImageList) 43 | if (dom && options.index === -1) { 44 | options.index = 0 45 | window.backupImageList.unshift({src: srcUrl, dom: dom}) 46 | console.log('Unshift image to list') 47 | } 48 | 49 | // build image viewer 50 | ImageViewer(window.backupImageList, options) 51 | 52 | // auto update 53 | let initComplete = false 54 | const initPeriod = 200 55 | 56 | let updateRelease = () => {} 57 | let updatePeriod = 500 58 | const multiplier = 1.2 59 | 60 | const initObserver = new MutationObserver(mutationList => { 61 | initComplete = mutationList.every(mutation => mutation.addedNodes.length === 0) 62 | }) 63 | initObserver.observe(document.body, {childList: true, subtree: true}) 64 | 65 | const container = ImageViewerUtils.getMainContainer() 66 | const updateObserver = new MutationObserver(async () => { 67 | let currentScrollX = container.scrollLeft 68 | let currentScrollY = container.scrollTop 69 | await new Promise(resolve => setTimeout(resolve, 50)) 70 | // check scroll complete 71 | while (currentScrollX !== container.scrollLeft || currentScrollY !== container.scrollTop) { 72 | currentScrollX = container.scrollLeft 73 | currentScrollY = container.scrollTop 74 | await new Promise(resolve => setTimeout(resolve, 200)) 75 | } 76 | updatePeriod = 500 77 | updateRelease() 78 | }) 79 | updateObserver.observe(document.body, {childList: true, subtree: true}) 80 | 81 | const unlazyObserver = new MutationObserver(mutationList => { 82 | const unlazyUpdate = mutationList.some(mutation => mutation.attributeName === 'iv-checking' && !mutation.target.hasAttribute('iv-checking')) 83 | if (unlazyUpdate || !document.body.classList.contains('iv-attached')) { 84 | updatePeriod = 500 85 | updateRelease() 86 | } 87 | }) 88 | unlazyObserver.observe(document.body, {childList: true, subtree: true, attributeFilter: ['iv-checking']}) 89 | 90 | while (document.body.classList.contains('iv-attached')) { 91 | // wait website init 92 | while (!initComplete) { 93 | initComplete = true 94 | await new Promise(resolve => setTimeout(resolve, initPeriod)) 95 | } 96 | if (!document.body.classList.contains('iv-attached')) return 97 | 98 | // update image viewer 99 | if (dom?.tagName === 'IMG') { 100 | ImageViewerUtils.updateWrapperSize(dom, domSize, options) 101 | } 102 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options) 103 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList) 104 | const currentImageList = ImageViewer('get_image_list') 105 | 106 | if (!document.body.classList.contains('iv-attached')) return 107 | if (combinedImageList.length > currentImageList.length || !ImageViewerUtils.isStrLengthEqual(combinedImageList, currentImageList)) { 108 | updatePeriod = 100 109 | window.backupImageList = Array.from(combinedImageList) 110 | ImageViewer(combinedImageList, options) 111 | } 112 | 113 | // wait website update 114 | await new Promise(resolve => { 115 | setTimeout(resolve, updatePeriod) 116 | updateRelease = resolve 117 | updatePeriod *= multiplier 118 | }) 119 | 120 | // wait visible 121 | while (document.visibilityState !== 'visible') { 122 | await new Promise(resolve => setTimeout(resolve, 100)) 123 | } 124 | } 125 | initObserver.disconnect() 126 | updateObserver.disconnect() 127 | unlazyObserver.disconnect() 128 | })() 129 | -------------------------------------------------------------------------------- /scripts/action-page.js: -------------------------------------------------------------------------------- 1 | ;(async function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | if (typeof ImageViewerUtils !== 'object') { 11 | await safeSendMessage('load_utility') 12 | } 13 | 14 | if (document.body.classList.contains('iv-attached')) { 15 | ImageViewer('close_image_viewer') 16 | return 17 | } 18 | 19 | // init 20 | const options = window.ImageViewerOption 21 | options.closeButton = true 22 | window.ImageViewerLastDom = null 23 | 24 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options) 25 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList) 26 | window.backupImageList = Array.from(combinedImageList) 27 | 28 | // build image viewer 29 | ImageViewer(window.backupImageList, options) 30 | 31 | // auto update 32 | let initComplete = false 33 | const initPeriod = 200 34 | 35 | let updateRelease = () => {} 36 | let updatePeriod = 500 37 | const multiplier = 1.2 38 | 39 | const initObserver = new MutationObserver(mutationList => { 40 | initComplete = mutationList.every(mutation => mutation.addedNodes.length === 0) 41 | }) 42 | initObserver.observe(document.body, {childList: true, subtree: true}) 43 | 44 | const container = ImageViewerUtils.getMainContainer() 45 | const updateObserver = new MutationObserver(async () => { 46 | let currentScrollX = container.scrollLeft 47 | let currentScrollY = container.scrollTop 48 | await new Promise(resolve => setTimeout(resolve, 50)) 49 | // check scroll complete 50 | while (currentScrollX !== container.scrollLeft || currentScrollY !== container.scrollTop) { 51 | currentScrollX = container.scrollLeft 52 | currentScrollY = container.scrollTop 53 | await new Promise(resolve => setTimeout(resolve, 200)) 54 | } 55 | updatePeriod = 500 56 | updateRelease() 57 | }) 58 | updateObserver.observe(document.body, {childList: true, subtree: true}) 59 | 60 | const unlazyObserver = new MutationObserver(mutationList => { 61 | const unlazyUpdate = mutationList.some(mutation => mutation.attributeName === 'iv-checking' && !mutation.target.hasAttribute('iv-checking')) 62 | if (unlazyUpdate || !document.body.classList.contains('iv-attached')) { 63 | updatePeriod = 500 64 | updateRelease() 65 | } 66 | }) 67 | unlazyObserver.observe(document.body, {childList: true, subtree: true, attributeFilter: ['iv-checking']}) 68 | 69 | while (document.body.classList.contains('iv-attached')) { 70 | // wait website init 71 | while (!initComplete) { 72 | initComplete = true 73 | await new Promise(resolve => setTimeout(resolve, initPeriod)) 74 | } 75 | if (!document.body.classList.contains('iv-attached')) return 76 | 77 | // update image viewer 78 | const orderedImageList = await ImageViewerUtils.getOrderedImageList(options) 79 | const combinedImageList = ImageViewerUtils.combineImageList(orderedImageList, window.backupImageList) 80 | const currentImageList = ImageViewer('get_image_list') 81 | 82 | if (!document.body.classList.contains('iv-attached')) return 83 | if (combinedImageList.length > currentImageList.length || !ImageViewerUtils.isStrLengthEqual(combinedImageList, currentImageList)) { 84 | updatePeriod = 100 85 | window.backupImageList = Array.from(combinedImageList) 86 | ImageViewer(combinedImageList, options) 87 | } 88 | 89 | // wait website update 90 | await new Promise(resolve => { 91 | setTimeout(resolve, updatePeriod) 92 | updateRelease = resolve 93 | updatePeriod *= multiplier 94 | }) 95 | 96 | // wait visible 97 | while (document.visibilityState !== 'visible') { 98 | await new Promise(resolve => setTimeout(resolve, 100)) 99 | } 100 | } 101 | initObserver.disconnect() 102 | updateObserver.disconnect() 103 | unlazyObserver.disconnect() 104 | })() 105 | -------------------------------------------------------------------------------- /scripts/activate-url.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | // image url mode 11 | function isImageContained(small, large) { 12 | const canvas1 = document.createElement('canvas') 13 | const canvas2 = document.createElement('canvas') 14 | const ctx1 = canvas1.getContext('2d') 15 | const ctx2 = canvas2.getContext('2d', {willReadFrequently: true}) 16 | 17 | // pooling 18 | const poolingWidth = 256 19 | const smallRatio = small.width / small.height 20 | const largeRatio = large.width / large.height 21 | 22 | canvas1.width = poolingWidth 23 | canvas1.height = poolingWidth / smallRatio 24 | ctx1.drawImage(small, 0, 0, small.width, small.height, 0, 0, poolingWidth, canvas1.height) 25 | 26 | canvas2.width = poolingWidth 27 | canvas2.height = poolingWidth / largeRatio 28 | ctx2.drawImage(large, 0, 0, large.width, large.height, 0, 0, poolingWidth, canvas2.height) 29 | 30 | // compare pixels 31 | const threshold = 0.5 32 | const base = ctx1.getImageData(0, 0, poolingWidth, canvas1.height).data 33 | const pixelCount = base.length / 4 34 | 35 | // check if center 36 | let diffCount = 0 37 | const center = ctx2.getImageData(0, (canvas2.height - canvas1.height) / 2, poolingWidth, canvas1.height).data 38 | for (let i = 0; i < base.length; i += 4) { 39 | if (Math.abs(base[i] - center[i]) + Math.abs(base[i + 1] - center[i + 1]) + Math.abs(base[i + 2] - center[i + 2]) > 24) { 40 | diffCount++ 41 | } 42 | } 43 | if (diffCount / pixelCount < threshold) return true 44 | 45 | // check if topmost 46 | diffCount = 0 47 | const top = ctx2.getImageData(0, 0, poolingWidth, canvas1.height).data 48 | for (let i = 0; i < base.length; i += 4) { 49 | if (Math.abs(base[i] - top[i]) + Math.abs(base[i + 1] - top[i + 1]) + Math.abs(base[i + 2] - top[i + 2]) > 24) { 50 | diffCount++ 51 | } 52 | } 53 | if (diffCount / pixelCount < threshold) return true 54 | 55 | return false 56 | } 57 | 58 | const argsRegex = /(.*?[=.](?:jpeg|jpg|png|gif|webp|bmp|tiff|avif))(?!\/)/i 59 | function getRawUrl(src) { 60 | if (src.startsWith('data') || src.startsWith('blob')) return src 61 | 62 | const filenameMatch = src.replace(/[-_]\d{3,4}x(?:\d{3,4})?\./, '.') 63 | if (filenameMatch !== src) return filenameMatch 64 | 65 | try { 66 | // protocol-relative URL 67 | const url = new URL(src, document.baseURI) 68 | const baseURI = url.origin + url.pathname 69 | 70 | const searchList = url.search 71 | .slice(1) 72 | .split('&') 73 | .filter(t => t.match(argsRegex)) 74 | .join('&') 75 | const imgSearch = searchList ? '?' + searchList : '' 76 | const rawSearch = baseURI + imgSearch 77 | 78 | const argsMatch = rawSearch.match(argsRegex) 79 | if (argsMatch) { 80 | const rawUrl = argsMatch[1] 81 | if (rawUrl !== src) return rawUrl 82 | } 83 | } catch (error) {} 84 | 85 | const argsMatch = src.match(argsRegex) 86 | if (argsMatch) { 87 | const rawUrl = argsMatch[1] 88 | if (rawUrl !== src) return rawUrl 89 | } 90 | return src 91 | } 92 | function getImage(rawUrl) { 93 | return new Promise(resolve => { 94 | const img = new Image() 95 | img.onload = () => resolve(img) 96 | img.onerror = () => resolve(img) 97 | img.src = rawUrl 98 | }) 99 | } 100 | function getUnlazyAttrList(img) { 101 | const src = img.currentSrc 102 | const rawUrl = getRawUrl(src) 103 | const attrList = [] 104 | attrList.push({name: 'raw url', value: rawUrl}) 105 | try { 106 | const url = new URL(src, document.baseURI) 107 | const pathname = url.pathname 108 | const search = url.search 109 | if (pathname.match(/[-_]thumb(?=nail)?\./)) { 110 | const nonThumbnailPath = pathname.replace(/[-_]thumb(?=nail)?\./, '.') 111 | const nonThumbnail = src.replace(pathname, nonThumbnailPath) 112 | attrList.push({name: 'non thumbnail path', value: nonThumbnail}) 113 | } 114 | 115 | if (!src.includes('?')) throw new Error() 116 | 117 | if (!pathname.includes('.')) { 118 | const extMatch = search.match(/jpeg|jpg|png|gif|webp|bmp|tiff|avif/) 119 | if (extMatch) { 120 | const filenameWithExt = pathname + '.' + extMatch[0] 121 | const rawExtension = src.replace(pathname + search, filenameWithExt) 122 | attrList.push({name: 'raw extension', value: rawExtension}) 123 | } 124 | } 125 | if (search.includes('width=') || search.includes('height=')) { 126 | const noSizeQuery = search.replace(/&?width=\d+|&?height=\d+/g, '') 127 | const rawQuery = src.replace(search, noSizeQuery) 128 | attrList.push({name: 'no size query', value: rawQuery}) 129 | } 130 | const noQuery = src.replace(pathname + search, pathname) 131 | attrList.push({name: 'no query', value: noQuery}) 132 | } catch (error) {} 133 | return attrList.filter(attr => attr.value !== src) 134 | } 135 | 136 | async function initImageViewer(image) { 137 | console.log('Start image mode') 138 | 139 | const options = window.ImageViewerOption 140 | options.closeButton = false 141 | options.minWidth = 0 142 | options.minHeight = 0 143 | 144 | await safeSendMessage('load_script') 145 | const imageDate = {src: image.src, dom: image} 146 | ImageViewer([imageDate], options) 147 | if (image.src.startsWith('data')) return 148 | 149 | const attrList = getUnlazyAttrList(image) 150 | for (const attr of attrList) { 151 | const rawImage = await getImage(attr.value) 152 | const rawSize = [rawImage.naturalWidth, rawImage.naturalHeight] 153 | if (image.naturalWidth > rawSize[0]) continue 154 | const rawRatio = rawSize[0] / rawSize[1] 155 | const currRatio = image.naturalWidth / image.naturalHeight 156 | // non trivial size or with proper ratio 157 | const nonTrivialSize = rawSize[0] % 10 || rawSize[1] % 10 158 | const properRatio = currRatio === 1 || Math.abs(rawRatio - currRatio) < 0.01 || rawRatio > 3 || rawRatio < 1 / 3 159 | const isRawCandidate = nonTrivialSize || properRatio 160 | if (isRawCandidate) { 161 | console.log(`Unlazy img with ${attr.name}`) 162 | const rawData = {src: attr.value, dom: image} 163 | ImageViewer([rawData], options) 164 | break 165 | } 166 | // sub image 167 | if (image.naturalWidth >= 256 && rawRatio < currRatio && isImageContained(image, rawImage)) { 168 | console.log(`Unlazy img with ${attr.name}`) 169 | const rawData = {src: attr.value, dom: image} 170 | ImageViewer([rawData], options) 171 | break 172 | } 173 | } 174 | } 175 | 176 | async function init() { 177 | // safe to send message in iframe 178 | if (window.top !== window.self) { 179 | safeSendMessage('load_extractor') 180 | return 181 | } 182 | 183 | await safeSendMessage('get_options') 184 | // Chrome may terminated service worker 185 | while (!window.ImageViewerOption) { 186 | await new Promise(resolve => setTimeout(resolve, 50)) 187 | await safeSendMessage('get_options') 188 | } 189 | 190 | try { 191 | const image = document.querySelector(`img[src='${location.href}']`) 192 | image ? initImageViewer(image) : safeSendMessage('load_worker') 193 | } catch (error) {} 194 | } 195 | 196 | if (document.visibilityState === 'visible') { 197 | init() 198 | } else { 199 | const handleEvent = () => { 200 | document.removeEventListener('visibilitychange', handleEvent) 201 | window.removeEventListener('focus', handleEvent) 202 | init() 203 | } 204 | document.addEventListener('visibilitychange', handleEvent) 205 | window.addEventListener('focus', handleEvent) 206 | } 207 | })() 208 | -------------------------------------------------------------------------------- /scripts/activate-worker.js: -------------------------------------------------------------------------------- 1 | ;(async function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | // init 11 | const options = window.ImageViewerOption 12 | const domainList = [] 13 | const regexList = [] 14 | for (const str of options.hoverCheckDisableList) { 15 | if (str[0] === '/' && str[str.length - 1] === '/') { 16 | regexList.push(str) 17 | } else { 18 | domainList.push(str) 19 | } 20 | } 21 | let disableHoverCheck = domainList.includes(location.hostname) 22 | disableHoverCheck ||= regexList.some(regex => regex.test(location.href)) 23 | 24 | if (window.top === window.self && !disableHoverCheck) { 25 | const styles = 'html.iv-worker-checking img {pointer-events: auto !important;} .disable-hover {pointer-events: none !important;}' 26 | const styleSheet = document.createElement('style') 27 | styleSheet.textContent = styles 28 | document.head.appendChild(styleSheet) 29 | } 30 | 31 | // image size 32 | const srcBitSizeMap = new Map() 33 | const srcRealSizeMap = new Map() 34 | const corsHostSet = new Set() 35 | const argsRegex = /(.*?[=.](?:jpeg|jpg|png|gif|webp|bmp|tiff|avif))(?!\/)/i 36 | 37 | async function fetchBitSize(url) { 38 | if (corsHostSet.has(url.hostname)) return 0 39 | try { 40 | const res = await fetch(url.href, {method: 'HEAD', signal: AbortSignal.timeout(5000)}) 41 | if (!res.ok) return 0 42 | if (res.redirected) return -1 43 | const type = res.headers.get('Content-Type') 44 | const length = res.headers.get('Content-Length') 45 | if (type?.startsWith('image') || (type === 'application/octet-stream' && url.href.match(argsRegex))) { 46 | const size = Number(length) 47 | return size 48 | } 49 | return 0 50 | } catch (error) { 51 | if (error.name !== 'TimeoutError') corsHostSet.add(url.hostname) 52 | return 0 53 | } 54 | } 55 | function getImageBitSize(src) { 56 | if (!src || src === 'about:blank' || src.startsWith('data')) return 0 57 | 58 | const cache = srcBitSizeMap.get(src) 59 | if (cache !== undefined) return cache 60 | 61 | const promise = new Promise(_resolve => { 62 | const resolve = size => { 63 | srcBitSizeMap.set(src, size) 64 | _resolve(size) 65 | } 66 | 67 | let waiting = false 68 | const updateSize = size => { 69 | if (size) resolve(size) 70 | else if (waiting) waiting = false 71 | else if (src.startsWith('blob')) return resolve(Number.MAX_SAFE_INTEGER) 72 | else resolve(0) 73 | } 74 | 75 | // protocol-relative URL 76 | const url = new URL(src, document.baseURI) 77 | const href = url.href 78 | if (url.hostname !== location.hostname) { 79 | waiting = true 80 | safeSendMessage({msg: 'get_size', url: href}).then(updateSize) 81 | } 82 | fetchBitSize(url).then(updateSize) 83 | }) 84 | 85 | srcBitSizeMap.set(src, promise) 86 | return promise 87 | } 88 | async function getImageRealSize(src) { 89 | const cache = srcRealSizeMap.get(src) 90 | if (cache !== undefined) return cache 91 | 92 | const promise = new Promise(_resolve => { 93 | const resolve = size => { 94 | srcRealSizeMap.set(src, size) 95 | _resolve(size) 96 | } 97 | 98 | const img = new Image() 99 | img.onload = () => resolve(Math.min(img.naturalWidth, img.naturalHeight)) 100 | img.onerror = () => resolve(0) 101 | setTimeout(() => img.complete || resolve(0), 10000) 102 | img.src = src 103 | }) 104 | 105 | srcRealSizeMap.set(src, promise) 106 | return promise 107 | } 108 | 109 | // image info 110 | const domSearcher = (function () { 111 | // searchImageFromTree 112 | function checkZIndex(e1, e2) { 113 | const e1zIndex = Number(window.getComputedStyle(e1).zIndex) 114 | const e2zIndex = Number(window.getComputedStyle(e2).zIndex) 115 | 116 | if (Number.isNaN(e1zIndex) || Number.isNaN(e2zIndex)) return 0 117 | if (e1zIndex > e2zIndex) { 118 | return -1 119 | } else if (e1zIndex < e2zIndex) { 120 | return 1 121 | } else { 122 | return 0 123 | } 124 | } 125 | function checkPosition(e1, e2) { 126 | const e1Rect = e1.getBoundingClientRect() 127 | const e2Rect = e2.getBoundingClientRect() 128 | 129 | const commonParent = e1.offsetParent || e1.parentNode 130 | const parentPosition = commonParent.getBoundingClientRect() 131 | 132 | const e1ActualPositionX = e1Rect.x - parentPosition.x 133 | const e1ActualPositionY = e1Rect.y - parentPosition.y 134 | const e2ActualPositionX = e2Rect.x - parentPosition.x 135 | const e2ActualPositionY = e2Rect.y - parentPosition.y 136 | 137 | if (e1ActualPositionY < e2ActualPositionY) { 138 | return -1 139 | } else if (e1ActualPositionY > e2ActualPositionY) { 140 | return 1 141 | } else if (e1ActualPositionX < e2ActualPositionX) { 142 | return -1 143 | } else { 144 | return 1 145 | } 146 | } 147 | function getTopElement(e1, e2) { 148 | // e1 -1, e2 1, same 0 149 | if (e1 === e2) return 0 150 | 151 | let result = checkZIndex(e1, e2) 152 | if (result !== 0) return result 153 | 154 | const e1Position = window.getComputedStyle(e1).position 155 | const e2Position = window.getComputedStyle(e2).position 156 | if (e1Position === 'absolute' || e2Position === 'absolute') { 157 | result = checkPosition(e1, e2) 158 | } else { 159 | result = e1.compareDocumentPosition(e2) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 160 | } 161 | return result 162 | } 163 | 164 | function getAllChildElements(node) { 165 | const result = [] 166 | const stack = [node] 167 | while (stack.length) { 168 | const current = stack.pop() 169 | for (const node of current.querySelectorAll('*')) { 170 | result.push(node) 171 | if (node.shadowRoot) { 172 | stack.push(node.shadowRoot) 173 | } 174 | } 175 | } 176 | return result 177 | } 178 | 179 | async function searchImageFromTree(dom, mouseX, mouseY) { 180 | if (!dom) return null 181 | 182 | let root = dom 183 | let prevSibling = root.previousElementSibling 184 | let nextSibling = root.nextElementSibling 185 | 186 | let rootClassList = root.classList.toString() 187 | let prevClassList = prevSibling && prevSibling.classList.toString() 188 | let nextClassList = nextSibling && nextSibling.classList.toString() 189 | 190 | let hasSameKindSibling = false 191 | hasSameKindSibling ||= prevSibling ? prevClassList === rootClassList || prevSibling.tagName === root.tagName : false 192 | hasSameKindSibling ||= nextSibling ? nextClassList === rootClassList || nextSibling.tagName === root.tagName : false 193 | while (!hasSameKindSibling && root.parentElement) { 194 | root = root.parentElement 195 | prevSibling = root.previousElementSibling 196 | nextSibling = root.nextElementSibling 197 | 198 | rootClassList = root.classList.toString() 199 | prevClassList = prevSibling && prevSibling.classList.toString() 200 | nextClassList = nextSibling && nextSibling.classList.toString() 201 | 202 | hasSameKindSibling ||= prevSibling ? prevClassList === rootClassList || prevSibling.tagName === root.tagName : false 203 | hasSameKindSibling ||= nextSibling ? nextClassList === rootClassList || nextSibling.tagName === root.tagName : false 204 | } 205 | 206 | const relatedDomList = [] 207 | const childList = getAllChildElements(root) 208 | for (const dom of childList) { 209 | const hidden = dom.offsetParent === null && dom.style.position !== 'fixed' 210 | if (hidden) { 211 | relatedDomList.push(dom) 212 | continue 213 | } 214 | const rect = dom.getBoundingClientRect() 215 | const inside = rect.left <= mouseX && rect.right >= mouseX && rect.top <= mouseY && rect.bottom >= mouseY 216 | if (inside) relatedDomList.push(dom) 217 | } 218 | 219 | const imageInfoList = [] 220 | for (const dom of relatedDomList) { 221 | const imageInfo = await extractImageInfo(dom) 222 | if (isImageInfoValid(imageInfo)) imageInfoList.push(imageInfo) 223 | } 224 | if (imageInfoList.length === 0) { 225 | return childList.length < 5 ? searchImageFromTree(root.parentElement, mouseX, mouseY) : null 226 | } 227 | if (imageInfoList.length === 1) return imageInfoList[0] 228 | const filteredImageInfoList = imageInfoList.filter(info => info[2].tagName === 'IMG') 229 | if (filteredImageInfoList.length === 1) return filteredImageInfoList[0] 230 | 231 | imageInfoList.sort((a, b) => { 232 | if (a[2].tagName === 'IMG' && b[2].tagName === 'IMG') return a[2].compareDocumentPosition(b[2]) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1 233 | if (a[2].tagName !== 'IMG' && b[2].tagName !== 'IMG') return getTopElement(a[2], b[2]) 234 | if (a[2].tagName === 'IMG') return -1 235 | if (b[2].tagName === 'IMG') return 1 236 | return 0 237 | }) 238 | const first = imageInfoList[0] 239 | const second = imageInfoList[1] 240 | const check = await isNewImageInfoBetter(first, second, mouseX, mouseY) 241 | return check ? first : second 242 | } 243 | 244 | // utility 245 | function extractNodeStyle(nodeStyle) { 246 | const backgroundImage = nodeStyle.backgroundImage 247 | if (backgroundImage === 'none') return null 248 | const bgList = backgroundImage.split(', ').filter(bg => bg.startsWith('url') && !bg.endsWith('.svg")')) 249 | if (bgList.length === 0) return null 250 | return bgList[0].slice(5, -2) 251 | } 252 | function getBackgroundURL(dom) { 253 | const nodeURL = extractNodeStyle(window.getComputedStyle(dom)) 254 | if (nodeURL) return nodeURL 255 | const beforeURL = extractNodeStyle(window.getComputedStyle(dom, '::before')) 256 | if (beforeURL) return beforeURL 257 | const afterURL = extractNodeStyle(window.getComputedStyle(dom, '::after')) 258 | if (afterURL) return afterURL 259 | return null 260 | } 261 | async function extractBackgroundInfo(dom, minSize) { 262 | const bgUrl = getBackgroundURL(dom) 263 | if (!bgUrl) return null 264 | const realMinSize = Math.min(minSize, await getImageRealSize(bgUrl)) 265 | return [bgUrl, realMinSize, dom] 266 | } 267 | async function extractImageInfo(dom) { 268 | const {width, height} = dom.getBoundingClientRect() 269 | if (dom.tagName === 'IMG') { 270 | // real time size and rendered size 271 | const sizeList = [width, height, dom.clientWidth, dom.clientHeight, dom.naturalWidth, dom.naturalHeight] 272 | const minSize = Math.min(...sizeList.filter(Boolean)) 273 | return [dom.currentSrc, minSize, dom] 274 | } 275 | const minSize = Math.min(width, height, dom.clientWidth, dom.clientHeight) 276 | if (dom.tagName === 'VIDEO' && dom.hasAttribute('poster')) { 277 | return [dom.poster, minSize, dom] 278 | } 279 | const bgInfo = extractBackgroundInfo(dom, minSize) 280 | return bgInfo 281 | } 282 | async function extractImageInfoFromTree(dom) { 283 | const domInfo = await extractImageInfo(dom) 284 | if (domInfo) return domInfo 285 | 286 | const allChildren = dom.querySelectorAll('*') 287 | if (allChildren.length < 5) { 288 | for (const children of allChildren) { 289 | const info = await extractImageInfo(children) 290 | if (info) return info 291 | } 292 | } 293 | return null 294 | } 295 | 296 | const isImageInfoValid = imageInfo => imageInfo !== null && imageInfo[0] !== '' && imageInfo[0] !== 'about:blank' 297 | const isNewImageInfoBetter = async (newInfo, oldInfo, mouseX, mouseY) => { 298 | if (oldInfo === null) return true 299 | // data url 300 | const newUrl = newInfo[0] 301 | const oldUrl = oldInfo[0] 302 | if (newUrl.startsWith('data')) return false 303 | if (oldUrl.startsWith('data')) return true 304 | // svg image 305 | const newIsSvg = newUrl.startsWith('data:image/svg') || newUrl.includes('.svg') 306 | const oldIsSvg = oldUrl.startsWith('data:image/svg') || oldUrl.includes('.svg') 307 | if (oldIsSvg && !newIsSvg) return true 308 | // element type 309 | const oldIsImage = oldInfo[2].tagName === 'IMG' || oldInfo[2].tagName === 'VIDEO' 310 | const newIsImage = newInfo[2].tagName === 'IMG' || newInfo[2].tagName === 'VIDEO' 311 | // placeholder 312 | const oldIsPlaceholder = oldInfo[1] < 10 313 | if (oldIsImage && !newIsImage && !oldIsPlaceholder) return false 314 | // partial background 315 | if (!oldIsImage && newIsImage && !oldIsPlaceholder) { 316 | const bgPos = window.getComputedStyle(oldInfo[2]).backgroundPosition 317 | const isPartialBackground = bgPos.split('px').map(Number).some(Boolean) 318 | return isPartialBackground ? newInfo[1] >= oldInfo[1] : true 319 | } 320 | // mouse position 321 | const newRect = newInfo[2].getBoundingClientRect() 322 | const oldRect = oldInfo[2].getBoundingClientRect() 323 | const newOffset = [mouseX - newRect.left - newRect.width / 2, mouseY - newRect.top - newRect.height / 2] 324 | const oldOffset = [mouseX - oldRect.left - oldRect.width / 2, mouseY - oldRect.top - oldRect.height / 2] 325 | const newDist = Math.sqrt(newOffset[0] ** 2 + newOffset[1] ** 2) 326 | const oldDist = Math.sqrt(oldOffset[0] ** 2 + oldOffset[1] ** 2) 327 | if (newDist > oldDist + 50) return false 328 | // size check 329 | const asyncList = [[newUrl, oldUrl].map(getImageBitSize), [newUrl, oldUrl].map(getImageRealSize)].flat() 330 | const [newBitSize, oldBitSize, newRealSize, oldRealSize] = await Promise.all(asyncList) 331 | if (newBitSize * oldBitSize !== 0) { 332 | return newBitSize / newRealSize > oldBitSize / oldRealSize 333 | } 334 | return newRealSize > oldRealSize 335 | } 336 | 337 | return { 338 | searchDomByPosition: async function (elementList, mouseX, mouseY) { 339 | let firstVisibleDom = null 340 | let imageInfoFromPoint = null 341 | let imageDomLayer = 0 342 | 343 | let hiddenImageInfoFromPoint = null 344 | let hiddenDomLayer = 0 345 | 346 | const maxTry = Math.min(20, elementList.length) 347 | let index = 0 348 | let tryCount = 0 349 | while (tryCount < maxTry) { 350 | const dom = elementList[index] 351 | const visible = dom.offsetParent !== null || dom.style.position === 'fixed' 352 | firstVisibleDom ??= visible ? dom : null 353 | const imageInfo = await (!imageInfoFromPoint ? extractImageInfoFromTree(dom) : extractImageInfo(dom)) 354 | const valid = isImageInfoValid(imageInfo) 355 | if (!valid) { 356 | index++ 357 | tryCount++ 358 | continue 359 | } 360 | if (!visible) { 361 | hiddenImageInfoFromPoint = imageInfo 362 | hiddenDomLayer = index++ 363 | tryCount = Math.max(maxTry - 5, ++tryCount) 364 | continue 365 | } 366 | const better = await isNewImageInfoBetter(imageInfo, imageInfoFromPoint, mouseX, mouseY) 367 | if (better) { 368 | imageInfoFromPoint = imageInfo 369 | imageDomLayer = index 370 | const url = imageInfoFromPoint[0] 371 | const svgImg = url.startsWith('data:image/svg') || url.includes('.svg') 372 | if (!svgImg) tryCount = Math.max(maxTry - 5, tryCount) 373 | } 374 | index++ 375 | tryCount++ 376 | } 377 | 378 | if (imageInfoFromPoint) { 379 | console.log(`Image node found, layer ${imageDomLayer}`) 380 | return imageInfoFromPoint 381 | } 382 | 383 | if (hiddenImageInfoFromPoint) { 384 | console.log(`Hidden image node found, layer ${hiddenDomLayer}`) 385 | return hiddenImageInfoFromPoint 386 | } 387 | 388 | const imageInfoFromTree = await searchImageFromTree(firstVisibleDom, mouseX, mouseY) 389 | if (isImageInfoValid(imageInfoFromTree)) { 390 | console.log('Image node found, hide under dom tree') 391 | return imageInfoFromTree 392 | } 393 | 394 | return null 395 | } 396 | } 397 | })() 398 | 399 | function deepGetElementFromPoint(x, y) { 400 | function createTravelTask(root) { 401 | // lazy evaluation 402 | return () => { 403 | const queue = [] 404 | const elementList = root.elementsFromPoint(x, y) 405 | for (const element of elementList) { 406 | if (visited.has(element) || visited.has(element.shadowRoot)) continue 407 | if (element.shadowRoot) { 408 | visited.add(element.shadowRoot) 409 | queue.push(createTravelTask(element.shadowRoot)) 410 | } else { 411 | visited.add(element) 412 | queue.push(element) 413 | } 414 | } 415 | return queue 416 | } 417 | } 418 | 419 | const result = [] 420 | const visited = new Set() 421 | const queue = [createTravelTask(document)] 422 | while (queue.length) { 423 | const current = queue.shift() 424 | if (typeof current === 'function') { 425 | queue.unshift(...current()) 426 | } else { 427 | result.push(current) 428 | } 429 | } 430 | return result 431 | } 432 | const getOrderedElement = (function () { 433 | return disableHoverCheck 434 | ? (mouseX, mouseY) => { 435 | // lock pointer event back to auto 436 | document.documentElement.classList.add('iv-worker-checking') 437 | // get all elements include hover 438 | const elementsBeforeDisableHover = deepGetElementFromPoint(mouseX, mouseY) 439 | // reset pointer event as default 440 | document.documentElement.classList.remove('iv-worker-checking') 441 | return elementsBeforeDisableHover 442 | } 443 | : async (mouseX, mouseY) => { 444 | // lock pointer event back to auto 445 | document.documentElement.classList.add('iv-worker-checking') 446 | // get all elements include hover 447 | const elementsBeforeDisableHover = deepGetElementFromPoint(mouseX, mouseY) 448 | // reset pointer event as default 449 | document.documentElement.classList.remove('iv-worker-checking') 450 | 451 | // disable hover 452 | const mouseLeaveEvent = new Event('mouseleave') 453 | for (const element of elementsBeforeDisableHover) { 454 | element.classList.add('disable-hover') 455 | element.dispatchEvent(mouseLeaveEvent) 456 | } 457 | // release priority and allow other script clear up hover element 458 | await new Promise(resolve => setTimeout(resolve, 10)) 459 | // clean up css 460 | for (const element of elementsBeforeDisableHover) { 461 | element.classList.remove('disable-hover') 462 | } 463 | // get all non hover elements 464 | const elementsAfterDisableHover = deepGetElementFromPoint(mouseX, mouseY) 465 | 466 | const stableElements = [] 467 | const unstableElements = [] 468 | for (const elem of elementsBeforeDisableHover) { 469 | if (elementsAfterDisableHover.includes(elem)) { 470 | stableElements.push(elem) 471 | } else { 472 | unstableElements.push(elem) 473 | } 474 | } 475 | const orderedElements = stableElements.concat(unstableElements) 476 | return orderedElements 477 | } 478 | })() 479 | async function getImageNodeInfo(mouseX, mouseY) { 480 | if (document.body.classList.contains('iv-attached')) return null 481 | const orderedElements = await getOrderedElement(mouseX, mouseY) 482 | const imageNodeInfo = await domSearcher.searchDomByPosition(orderedElements, mouseX, mouseY) 483 | return imageNodeInfo 484 | } 485 | 486 | const markingDom = (function () { 487 | return window.top === window.self 488 | ? dom => { 489 | window.ImageViewerLastDom = dom 490 | } 491 | : () => safeSendMessage('reset_dom') 492 | })() 493 | 494 | document.addEventListener( 495 | 'contextmenu', 496 | async e => { 497 | window.ImageViewerLastDom = undefined 498 | 499 | // release priority and allow contextmenu work properly 500 | await new Promise(resolve => setTimeout(resolve, 0)) 501 | const imageNodeInfo = await getImageNodeInfo(e.clientX, e.clientY) 502 | if (imageNodeInfo === null) { 503 | markingDom(null) 504 | return 505 | } 506 | markingDom(imageNodeInfo[2]) 507 | 508 | // display image dom 509 | console.log(imageNodeInfo.pop()) 510 | 511 | safeSendMessage({msg: 'update_info', data: imageNodeInfo}) 512 | }, 513 | true 514 | ) 515 | })() 516 | -------------------------------------------------------------------------------- /scripts/download-images.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | // zip 11 | function generateCRCTable() { 12 | const crcTable = new Uint32Array(256) 13 | const polynomial = 0xedb88320 // CRC-32 polynomial 14 | 15 | for (let i = 0; i < 256; i++) { 16 | let crc = i 17 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 18 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 19 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 20 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 21 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 22 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 23 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 24 | crc = crc & 1 ? (crc >>> 1) ^ polynomial : crc >>> 1 25 | crcTable[i] = crc 26 | } 27 | return crcTable 28 | } 29 | function calculateCRC32(data) { 30 | const crcTable = generateCRCTable() 31 | let crc = 0xffffffff // Initial CRC value 32 | 33 | for (let i = 0; i < data.length; i++) { 34 | const byte = data[i] 35 | crc = (crc >>> 8) ^ crcTable[(crc ^ byte) & 0xff] 36 | } 37 | 38 | crc ^= 0xffffffff // Final XOR value 39 | const crcBytes = new Uint8Array(4) 40 | crcBytes[0] = (crc >> 24) & 0xff 41 | crcBytes[1] = (crc >> 16) & 0xff 42 | crcBytes[2] = (crc >> 8) & 0xff 43 | crcBytes[3] = crc & 0xff 44 | 45 | return crcBytes 46 | } 47 | 48 | function buildLocalFileHeader(filename, data) { 49 | const crc32 = calculateCRC32(data) 50 | const compressedSize = data.length 51 | const encoder = new TextEncoder() 52 | const filenameBytes = encoder.encode(filename) 53 | const filenameLength = filenameBytes.length 54 | 55 | // Construct the local file header 56 | const localFileHeader = new Uint8Array(30 + filenameLength + compressedSize) 57 | const view = new DataView(localFileHeader.buffer) 58 | 59 | // Little-endian byte order 60 | view.setUint32(0, 0x04034b50, true) // Local file header signature 61 | view.setUint16(4, 20, true) // Version needed to extract (minimum) 62 | view.setUint16(6, 0, true) // No special flags 63 | view.setUint16(8, 0, true) // No compression 64 | view.setUint16(10, 0, true) // Placeholder for modification time (not used) 65 | view.setUint16(12, 0, true) // Placeholder for modification date (not used) 66 | view.setUint32(14, crc32, true) // CRC-32 of uncompressed data 67 | view.setUint32(18, compressedSize, true) // Compressed size 68 | view.setUint32(22, compressedSize, true) // Uncompressed size 69 | view.setUint16(26, filenameLength, true) // File name length 70 | view.setUint16(28, 0, true) // No extra fields 71 | localFileHeader.set(filenameBytes, 30) // File name 72 | localFileHeader.set(data, 30 + filenameLength) // File data 73 | 74 | return localFileHeader 75 | } 76 | function buildCentralDirectory(localFileHeader, offset) { 77 | const headerView = new DataView(localFileHeader.buffer) 78 | const filenameLength = headerView.getUint16(26, true) 79 | const headerData = localFileHeader.subarray(4, 30) 80 | const fileName = localFileHeader.subarray(30, 30 + filenameLength) 81 | 82 | // Construct the central directory entry 83 | const centralDirectoryEntry = new Uint8Array(46 + filenameLength) 84 | const view = new DataView(centralDirectoryEntry.buffer) 85 | 86 | // Little-endian byte order 87 | view.setUint32(0, 0x02014b50, true) // Central directory file header signature 88 | view.setUint16(4, 20, true) // Version made by 89 | centralDirectoryEntry.set(headerData, 6) // CDE 6-32 = LFH 4-30 90 | view.setUint16(32, 0, true) // No file comment 91 | view.setUint16(34, 0, true) // Disk number start 92 | view.setUint16(36, 0, true) // Internal file attributes 93 | view.setUint32(38, 0, true) // External file attributes 94 | view.setUint32(42, offset, true) // Relative offset of local file header 95 | centralDirectoryEntry.set(fileName, 46) // File name 96 | 97 | return centralDirectoryEntry 98 | } 99 | 100 | function buildZip(localFileHeaderList) { 101 | const centralDirectoryList = [] 102 | let centralOffset = 0 103 | 104 | // Build central directory entries 105 | for (let i = 0; i < localFileHeaderList.length; i++) { 106 | const localFileHeader = localFileHeaderList[i] 107 | const centralDirectoryEntry = buildCentralDirectory(localFileHeader, centralOffset) 108 | centralDirectoryList.push(centralDirectoryEntry) 109 | centralOffset += localFileHeader.length 110 | } 111 | 112 | // Calculate the size of the central directory 113 | const centralDirectorySize = centralDirectoryList.reduce((total, entry) => total + entry.length, 0) 114 | 115 | // Build the end of central directory record 116 | const endOfCentralDirectoryRecord = new Uint8Array(22) 117 | const view = new DataView(endOfCentralDirectoryRecord.buffer) 118 | 119 | view.setUint32(0, 0x06054b50, true) // End of central directory signature 120 | view.setUint16(4, 0, true) // Number of this disk 121 | view.setUint16(6, 0, true) // Disk where central directory starts 122 | view.setUint16(8, localFileHeaderList.length, true) // Number of central directory records on this disk 123 | view.setUint16(10, localFileHeaderList.length, true) // Total number of central directory records 124 | view.setUint32(12, centralDirectorySize, true) // Size of central directory 125 | view.setUint32(16, centralOffset, true) // Offset of start of central directory 126 | view.setUint16(20, 0, true) // No comment 127 | 128 | // Combine all the components into the final zip file 129 | const zipSize = centralOffset + centralDirectorySize + endOfCentralDirectoryRecord.length 130 | const zipFile = new Uint8Array(zipSize) 131 | 132 | let offset = 0 133 | for (const localFileHeader of localFileHeaderList) { 134 | zipFile.set(localFileHeader, offset) 135 | offset += localFileHeader.length 136 | } 137 | 138 | for (const centralDirectoryEntry of centralDirectoryList) { 139 | zipFile.set(centralDirectoryEntry, offset) 140 | offset += centralDirectoryEntry.length 141 | } 142 | 143 | zipFile.set(endOfCentralDirectoryRecord, offset) 144 | 145 | return zipFile 146 | } 147 | 148 | // utility 149 | function getUserSelection(length) { 150 | if (length === 1) return [true] 151 | 152 | const userSelection = prompt("Images to Download: eg. '1-5, 8, 11-13'", `1-${length}`) 153 | if (!userSelection) return null 154 | 155 | const input = userSelection.replaceAll(' ', '') 156 | const regex = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/ 157 | if (!regex.test(input)) { 158 | alert('Invalid selection.') 159 | return null 160 | } 161 | 162 | const result = new Array(length).fill(false) 163 | const processedInput = input.split(',') 164 | for (const part of processedInput) { 165 | if (part.includes('-')) { 166 | const [start, end] = part 167 | .split('-') 168 | .map(n => Math.min(length, Math.max(1, Number(n))) - 1) 169 | .sort((a, b) => a - b) 170 | for (let i = start; i <= end; i++) { 171 | result[i] = true 172 | } 173 | } else { 174 | const index = Math.min(length, Math.max(1, Number(part))) - 1 175 | result[index] = true 176 | } 177 | } 178 | 179 | return result 180 | } 181 | function getImageBinary(url) { 182 | return fetch(url) 183 | .then(response => response.arrayBuffer()) 184 | .then(arrayBuffer => new Uint8Array(arrayBuffer)) 185 | .catch(async () => { 186 | const [dataUrl] = await safeSendMessage({msg: 'request_cors_url', url: url}) 187 | const res = await fetch(dataUrl) 188 | const rawArray = await res.arrayBuffer() 189 | return new Uint8Array(rawArray) 190 | }) 191 | } 192 | 193 | // main 194 | async function main() { 195 | const imageList = ImageViewer('get_image_list') 196 | if (imageList.length === 0) return 197 | 198 | const imageUrlList = imageList.map(img => img.src) 199 | const selectionRange = getUserSelection(imageUrlList.length) 200 | if (selectionRange === null) return 201 | 202 | const selectedUrlList = imageUrlList.map((v, i) => [v, i]).filter(item => selectionRange[item[1]]) 203 | if (selectedUrlList.length === 0) return 204 | 205 | const imageBinaryList = await Promise.all(selectedUrlList.map(async item => [await getImageBinary(item[0]), item[1]])) 206 | 207 | const localFileHeaderList = [] 208 | for (const [data, index] of imageBinaryList) { 209 | const indexString = ('0000' + (index + 1)).slice(-5) 210 | const url = imageUrlList[index] 211 | const name = url.startsWith('data') ? '' : '_' + url.split('?')[0].split('/').at(-1) 212 | const extension = name.includes('.') ? '' : '.jpg' 213 | const filename = indexString + name + extension 214 | 215 | const localFileHeader = buildLocalFileHeader(filename, data) 216 | localFileHeaderList.push(localFileHeader) 217 | } 218 | const zip = buildZip(localFileHeaderList) 219 | const blob = new Blob([zip.buffer]) 220 | 221 | const a = document.createElement('a') 222 | a.href = URL.createObjectURL(blob, 'application/zip') 223 | a.download = `ImageViewer_${Date.now()}_${document.title}.zip` 224 | a.click() 225 | URL.revokeObjectURL(a.href) 226 | } 227 | 228 | main() 229 | })() 230 | -------------------------------------------------------------------------------- /scripts/extract-iframe.js: -------------------------------------------------------------------------------- 1 | window.ImageViewerExtractor = (function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | async function getSubFrameRedirectedHref() { 11 | const subFrame = document.getElementsByTagName('iframe') 12 | const subFrameHref = [...subFrame].map(iframe => iframe.src) 13 | const subFrameRedirectedHref = subFrameHref.length ? await safeSendMessage({msg: 'get_redirect', data: subFrameHref}) : [] 14 | return subFrameRedirectedHref 15 | } 16 | 17 | function isNodeSizeEnough(node, minWidth, minHeight) { 18 | const widthAttr = node.getAttribute('iv-width') 19 | const heightAttr = node.getAttribute('iv-height') 20 | if (widthAttr && heightAttr) { 21 | const width = Number(widthAttr) 22 | const height = Number(heightAttr) 23 | return width >= minWidth && height >= minHeight 24 | } 25 | const {width, height} = node.getBoundingClientRect() 26 | if (width === 0 || height === 0) { 27 | node.setAttribute('no-bg', '') 28 | return false 29 | } 30 | node.setAttribute('iv-width', width) 31 | node.setAttribute('iv-height', height) 32 | return width >= minWidth && height >= minHeight 33 | } 34 | function deepQuerySelectorAll(target, selector) { 35 | const result = [] 36 | const stack = [target] 37 | const visited = [] 38 | while (stack.length) { 39 | const current = stack.pop() 40 | // check shadowRoot 41 | for (const node of current.querySelectorAll('*:not([no-shadow])')) { 42 | if (node.shadowRoot) { 43 | stack.push(node.shadowRoot) 44 | } else { 45 | visited.push(node) 46 | } 47 | } 48 | result.push(...current.querySelectorAll(selector)) 49 | } 50 | for (const node of visited) { 51 | node.setAttribute('no-shadow', '') 52 | } 53 | return result 54 | } 55 | function getImageList(options) { 56 | const minWidth = options.minWidth || 0 57 | const minHeight = options.minHeight || 0 58 | const imageList = [] 59 | 60 | const rawImageList = deepQuerySelectorAll(document.body, 'img') 61 | for (const img of rawImageList) { 62 | // only client size should be checked in order to bypass large icon or hidden image 63 | const {width, height} = img.getBoundingClientRect() 64 | if ((width >= minWidth && height >= minHeight) || img === window.ImageViewerLastDom) { 65 | // currentSrc might be empty during unlazy or update 66 | const imgSrc = img.currentSrc || img.src 67 | imageList.push(imgSrc) 68 | } 69 | } 70 | 71 | const videoList = deepQuerySelectorAll(document.body, 'video[poster]') 72 | for (const video of videoList) { 73 | const {width, height} = video.getBoundingClientRect() 74 | if (width >= minWidth && height >= minHeight) { 75 | imageList.push(video.poster) 76 | } 77 | } 78 | 79 | const uncheckedNodeList = deepQuerySelectorAll(document.body, '*:not([no-bg])') 80 | if (!document.body.hasAttribute('no-bg')) uncheckedNodeList.push(document.body) 81 | for (const node of uncheckedNodeList) { 82 | if (!isNodeSizeEnough(node, minWidth, minHeight)) continue 83 | const attrUrl = node.getAttribute('iv-bg') 84 | if (attrUrl !== null) { 85 | imageList.push(attrUrl) 86 | continue 87 | } 88 | const nodeStyle = window.getComputedStyle(node) 89 | const backgroundImage = nodeStyle.backgroundImage 90 | if (backgroundImage === 'none') { 91 | node.setAttribute('no-bg', '') 92 | continue 93 | } 94 | const bgList = backgroundImage.split(', ').filter(bg => bg.startsWith('url') && !bg.endsWith('.svg")')) 95 | if (bgList.length === 0) { 96 | node.setAttribute('no-bg', '') 97 | continue 98 | } 99 | const url = bgList[0].slice(5, -2) 100 | node.setAttribute('iv-bg', url) 101 | imageList.push(url) 102 | } 103 | 104 | return options.svgFilter 105 | ? [...new Set(imageList)].filter(url => url !== '' && url !== 'about:blank' && !url.includes('.svg')) 106 | : [...new Set(imageList)].filter(url => url !== '' && url !== 'about:blank') 107 | } 108 | function getCanvasList(options) { 109 | const minWidth = options.minWidth || 0 110 | const minHeight = options.minHeight || 0 111 | const canvasList = [] 112 | 113 | const rawCanvasList = deepQuerySelectorAll(document.body, 'canvas') 114 | for (const canvas of rawCanvasList) { 115 | const {width, height} = canvas.getBoundingClientRect() 116 | if (width >= minWidth && height >= minHeight) { 117 | const dataUrl = canvas.toDataURL() 118 | if (dataUrl === 'data:,') continue 119 | canvasList.push(dataUrl) 120 | } 121 | } 122 | return canvasList 123 | } 124 | 125 | return { 126 | extractImage: async function (options) { 127 | const subFrameRedirectedHref = await getSubFrameRedirectedHref() 128 | const imageList = options.canvasMode ? getCanvasList(options) : getImageList(options) 129 | return [location.href, subFrameRedirectedHref, imageList] 130 | } 131 | } 132 | })() 133 | -------------------------------------------------------------------------------- /scripts/hook.js: -------------------------------------------------------------------------------- 1 | ;(function () { 2 | 'use strict' 3 | 4 | const safeSendMessage = function (...args) { 5 | if (chrome.runtime?.id) { 6 | return chrome.runtime.sendMessage(...args) 7 | } 8 | } 9 | 10 | // prevent image blob revoked 11 | const isImageUrlMap = new Map() 12 | const realCreate = URL.createObjectURL 13 | const realRevoke = URL.revokeObjectURL 14 | URL.createObjectURL = function (obj) { 15 | const url = realCreate(obj) 16 | 17 | if (!(obj instanceof Blob)) { 18 | isImageUrlMap.set(url, false) 19 | return url 20 | } 21 | if (obj.type.startsWith('image/')) { 22 | isImageUrlMap.set(url, true) 23 | return url 24 | } 25 | if (obj.size > 1024 * 1024 * 5 || (obj.type !== '' && obj.type !== 'application/octet-stream')) { 26 | isImageUrlMap.set(url, false) 27 | return url 28 | } 29 | const promise = new Promise(_resolve => { 30 | const resolve = result => { 31 | _resolve(result) 32 | isImageUrlMap.set(url, result) 33 | } 34 | const img = new Image() 35 | img.onload = () => resolve(true) 36 | img.onerror = () => resolve(false) 37 | img.src = url 38 | }) 39 | isImageUrlMap.set(url, promise) 40 | return url 41 | } 42 | URL.revokeObjectURL = async function (url) { 43 | const isImage = await isImageUrlMap.get(url) 44 | if (!isImage) realRevoke(url) 45 | } 46 | 47 | // prevent canvas tainted 48 | async function getImageBase64(image) { 49 | try { 50 | const res = await fetch(image.src) 51 | if (res.ok) { 52 | const blob = await res.blob() 53 | const reader = new FileReader() 54 | const dataUrl = await new Promise(resolve => { 55 | reader.onload = () => resolve(reader.result) 56 | reader.readAsDataURL(blob) 57 | }) 58 | return dataUrl 59 | } 60 | } catch (error) {} 61 | // wake up background 62 | while (true) { 63 | if (await safeSendMessage({msg: 'ping'})) break 64 | await new Promise(resolve => setTimeout(resolve, 50)) 65 | } 66 | const [dataUrl] = await safeSendMessage({msg: 'request_cors_url', url: image.src}) 67 | return dataUrl 68 | } 69 | async function getBase64Image(image) { 70 | const dataUrl = await getImageBase64(image) 71 | const dataImage = new Image() 72 | const result = await new Promise(resolve => { 73 | dataImage.onload = resolve(true) 74 | dataImage.onerror = resolve(false) 75 | dataImage.src = dataUrl 76 | }) 77 | // return empty image on failure 78 | return result ? dataImage : new Image() 79 | } 80 | function checkCORS(image) { 81 | if (image.crossOrigin === 'anonymous') return false 82 | const canvas = document.createElement('canvas') 83 | canvas.width = 100 84 | canvas.height = 100 85 | const ctx = canvas.getContext('2d') 86 | realDrawImage.apply(ctx, [image, 0, 0]) 87 | try { 88 | canvas.toDataURL('image/png') 89 | return false 90 | } catch (error) {} 91 | return true 92 | } 93 | 94 | const realDrawImage = CanvasRenderingContext2D.prototype.drawImage 95 | CanvasRenderingContext2D.prototype.drawImage = async function (...args) { 96 | if (args[0] instanceof HTMLImageElement) { 97 | this.canvas.cors = this.canvas.cors || checkCORS(args[0]) 98 | if (this.canvas.cors) { 99 | args[0] = await getBase64Image(args[0]) 100 | } 101 | } 102 | return realDrawImage.apply(this, args) 103 | } 104 | })() 105 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 1.41 [2025-02-06]: 2 | Stability update 3 | This version was originally planned as a major update, but the development of new features was delayed 4 | 1. Added a hotkey command for canvas mode 5 | 2. Added support for background images in pseudo elements 6 | 3. Improved scroll unlazy logic 7 | 4. Improved right click image size referencing logic 8 | 5. Other bug fixes and improvements 9 | P.S. Starting with this version, the extension will also be published on addons.mozilla.org for Firefox users 10 | 11 | 1.40 [2024-10-14]: 12 | 1. Added a new option to allow users to disable image unlazy on specific domains 13 | 2. Introduced a web demo, enabling users to try the feature before installation 14 | 3. Updated the options page and added a simple support page 15 | 4. Fixed a bug in the new view canvas feature that caused issues on sites like Notion 16 | 17 | 1.39.1 [2024-10-01]: 18 | Patch Update 19 | 1. Fixed a bug in the new view canvas features that caused issues with some sites like Google Sheets 20 | 21 | 1.39 [2024-09-29]: 22 | Major Update 23 | 1. Added an action to the icon context menu allowing users to view canvas elements 24 | // Note: This feature only supports snapshots, not GIF creation 25 | // May also be useful for cases where an image is visible but not accessible in normal mode 26 | // This could include an image drawn on a canvas element 27 | 2. Added support for local and blob images to mainstream reverse search 28 | 3. Fixed navigation, it will now correctly wait for images to be rendered on the screen 29 | 4. The space bar can now be used to send a middle click to an image (previously only "0" could be used) 30 | 5. Other bug fixes and improvements 31 | 32 | 1.38 [2024-09-09]: 33 | Stability update 34 | 1. Added support for data URL images to mainstream reverse search 35 | 2. Fixed a bug that could change the website's default layout 36 | 3. Fixed a bug that could toggle the website's default hotkeys (eg. page navigation) 37 | 4. Other bug fixes and improvements 38 | 39 | 1.37 [2024-08-11]: 40 | Performance and Stability update 41 | 1. The control panel will now auto hide after 1.5 seconds of mouse hover 42 | // move cursor over buttons will toggle the panel again 43 | // provides clearer view when using scroll to view image 44 | 2. Improved image viewer's logic for build/update image list 45 | 3. Refactored image collection logic to enhance stability of the image list 46 | 4. Rewritten auto scroll logic to ensure no images are skipped 47 | 5. Enhanced code quality 48 | 6. Other bug fixes and improvements 49 | 50 | 1.36 [2024-07-22]: 51 | Major Update 52 | 1. Added a hotkey for auto navigation (shift + arrow keys) 53 | 2. Added ton of code to support of custom element 54 | 3. Add sub-image check to improve image unlazy in url mode 55 | 4. Improve and refactor iframe image extraction logic 56 | 5. Improve CSS and layout of the image viewer 57 | 6. Refactor data structure for image info 58 | 7. Other bug fixes and improvements 59 | 60 | 1.35 [2024-07-03]: 61 | 1. Reduced zoom & rotate transition flash 62 | 2. Improved auto update logic 63 | 3. Enhanced ability to find larger size raw images 64 | 4. Reworked unlazy logic, no longer need to wait when reopening within a short time 65 | 5. Reworked iframe logic, can now handle iframe in iframe cases 66 | 6. Fixed a bug that changed current index after image list update 67 | 7. Other bug fixes and improvements 68 | 69 | 1.34 [2024-04-11]: 70 | Stability update 71 | 1. Prevented image loading flash in URL mode 72 | 2. Add smooth transition for image transform 73 | 3. Fixed a bug where AltGraph could not be used with Ctrl in hotkey combinations 74 | // related hotkey: image transformation and image reverse search 75 | 4. Improved code performance 76 | 5. Implemented error handling to minimize minor errors displayed to users 77 | 6. Added support for new type of unlazy (simulate mouse hover) 78 | 7. Other bug fixes and improvements 79 | 80 | 1.33 [2024-01-15]: 81 | Functional Update 82 | 1. Added a new default fit mode option: "Original size (does not exceed window)" 83 | 2. Added a maximum size limit (3x) for other fit modes to prevent enlarging small images too much 84 | 3. Added a new hotkey (Shift + B) for switching the background color: transparent -> black -> white 85 | 4. Added new hotkeys for image transformation: 86 | - Move: Ctrl + Alt + ↑↓←→ / WASD 87 | - Zoom: Alt + ↑↓ / WS 88 | - Rotate: Alt + ←→ / AD 89 | 5. Improved auto-scrolling 90 | 6. Added support for more edge cases 91 | 7. Other bug fixes and improvements 92 | 93 | 1.32 [2023-12-31]: 94 | 1. Improved accuracy of image middle click redirect 95 | 2. Enhanced size filter referencing of picking an image by right click 96 | 3. Solve CSS issues related to lazy images on some websites 97 | 4. Added support for the embed element 98 | 5. Refactored and improved code logic 99 | 6. Other bug fixes and improvements 100 | 101 | 1.31 [2023-10-22]: 102 | 1. Rotation now rotates around the center of the viewpoint 103 | 2. Auto scroll hotkey will toggle auto scroll instead of just starting it 104 | 3. Navigation with "WASD" is now supported 105 | 4. Support fast navigation by pressing the Ctrl key at the same time to activate it 106 | 5. Support memory of last image when restarting in page mode 107 | 6. Enhanced code quality 108 | 7. Other bug fixes and improvements 109 | 110 | 1.30 [2023-08-27]: 111 | Stability update 112 | 1. Improved SVG filtering 113 | 2. Added support for multiple layers unlazy 114 | 3. Enhanced logic for getting raw image URL 115 | 4. Added support for more edge cases 116 | 5. Other bug fixes and improvements 117 | 118 | 1.29 [2023-08-08]: 119 | 1. Corrected code related to the service worker lifecycle 120 | 2. Enhanced unlazy logic to handle additional cases 121 | 3. Improved logic for updating the size filter when there are images of the same kind as the picked image 122 | 4. Enhanced the user experience on auto scroll 123 | 5. Numerous bug fixes and minor improvements 124 | 125 | 1.28 [2023-07-10]: 126 | Major Update 127 | 1. Added a hotkey to manually enable auto scroll 128 | 2. Added a hotkey to download images collected by image viewer 129 | // Note: This extension is not a resource downloader 130 | // Download functionality is limited to basic features 131 | // eg. selecting a download range and packaging in a zip file 132 | 3. Improved first display time of image viewer 133 | 4. Improved middle-click redirect to open the original image's hyperlink 134 | 5. Improved correctness of right click image pickup 135 | 6. Other bug fixes and improvements 136 | 137 | 1.27 [2023-07-06]: 138 | 1. Improved image selection, decrease the priority of image placeholder and image sprite 139 | 2. Improved border display after using moveTo 140 | 3. Improved auto scrolling and auto update 141 | 4. Some bug fixes 142 | 143 | 1.26 [2023-06-17]: 144 | 1. Improved the logic of using middle click to open the link of current image 145 | 2. Fixed a bug that caused jumping in viewer index 146 | 3. Fixed a bug that prevented the image viewer from automatically starting for image URLs 147 | 4. Other bug fixes and improvements 148 | 149 | 1.25 [2023-06-04]: 150 | 1. AltGraph key now functions the same as Alt key in hotkey 151 | 2. More intuitive zoom, where zooming now occurs at the screen center instead of the image center 152 | 3. Fixed the incorrect position of the border display after the moveTo operation 153 | 4. Fixed a bug that caused a conflict in the scroll function 154 | 5. Added a check for iframes to handle a bug in Chrome 155 | 6. Removed code that caused extra rendering time 156 | 7. Added caching to enhance performance 157 | 8. Improved performance on right click image pickup 158 | 159 | 1.24 [2023-06-01]: 160 | 1. Introduces method for old style lazy image 161 | 2. Enhance the moveTo function and label border 162 | 3. Improve stability of the extension 163 | 4. Fix bugs and improve performance 164 | 165 | 1.23 [2023-05-29]: 166 | 1. Add temporary image list storage 167 | 2. Significantly reduced startup time by approximately 3-10 times 168 | 3. Refine UI 169 | 4. Improve code logic 170 | 171 | 1.22 [2023-05-28]: 172 | 1. Support deeper-layer iframes 173 | 2. Enhance the moveTo function 174 | 3. Revamp border display following moveTo 175 | 4. Support additional edge cases 176 | 5. Improve performance and fix bugs 177 | 178 | 1.21 [2023-05-27]: 179 | 1. Fixed a bug when getting the image list, so it won't repeat the same image with different sizes 180 | 2. Fixed the "moveTo" button, now it functions correctly on websites like Instagram and Twitter 181 | 3. Fixed the image update, so it won't jump back to the first image when updating 182 | 4. Fixed a bug related to image looping, now it will wait for an image update when it reaches the end 183 | 184 | 1.20 [2023-05-26]: 185 | 1. Improved auto update and auto scroll 186 | 2. More stability on image file URLs 187 | 3. Added support for more iframe images 188 | 4. Improved performance and fixed bugs 189 | 190 | 1.19 [2023-05-14]: 191 | 1. Improve the stability of auto scroll 192 | 2. Improve the code logic for better performance 193 | 3. Fix a lot of bugs 194 | 195 | 1.18 [2023-05-04]: 196 | 1. Support auto scroll 197 | 2. Add options to enable auto scroll and disable hover check 198 | 3. Refactor code for better readability 199 | 4. Fix bug related to hover check and other minor bugs 200 | 201 | 1.17 [2023-04-30]: 202 | Stability update 203 | 1. Add some code to increase the stability 204 | 2. Add handle to more edge cases 205 | 3. Fix bugs 206 | 207 | 1.16 [2023-04-10]: 208 | 1. Image viewer now collects images after website adding new content 209 | // usually website update is toggled by scroll to the end of the page 210 | // you can archive it by scrolling on the scrollbar or press "End" key on keyboard 211 | // you may also use other "next page" script/extension 212 | 2. Fix issues for youtube thumbnail 213 | 3. Fix bugs related to last update 214 | 4. Refactor code to improving program structure 215 | 216 | 1.15 [2023-04-05]: 217 | Large Update 218 | 1. Add support on update image in the viewer 219 | 2. Solve the problem for image viewer can't be open on some websites 220 | 3. Fix CORS issues for iframe images 221 | 4. Fix other issues in rare situations 222 | 5. Improve performance and fix some bugs 223 | 224 | 1.14 [2023-04-01]: 225 | 1. Improve CSS of image viewer 226 | 2. Improve performance of right click image pickup 227 | 3. Add an icon image pre-check before unlazy image to improve performance 228 | 4. Enhance the method of getting image wrapper size 229 | 5. Bug fixes 230 | 231 | 1.13 [2023-03-18]: 232 | 1. Improve right click image pickup performance 233 | 2. Improve stability on image unlazy 234 | 3. Extend the loading time limit for images inside image viewer 235 | 4. Fix lot of typos and bugs 236 | 237 | 1.12 [2023-02-14]: 238 | 1. Add this popup page to show release notes when install or update 239 | 2. Improve stability 240 | 3. Add domain white list for image unlazy 241 | // create issues on github if you want to add domain to the list 242 | // may move to option page or just hide in source code 243 | 244 | 1.11 [2023-02-11]: 245 | 1. Images are now order by its real location 246 | 3. No longer use dataURL, ObjectURL is faster and better for the browser to render images 247 | 2. Min size filter will also considers wrapper of the selected image 248 | 4. Some website that disabled right click menu. Add "view last right click" in icon menu to handle it 249 | 250 | 1.10 [2023-02-11]: 251 | 1. Add MoveTo support for iframe images 252 | 2. Improve right click image pickup 253 | 3. Improve image check size method 254 | 255 | 1.9 [2023-01-13]: 256 | 1. Support image pickup using right click 257 | 2. Delay execution of worker script to improve performance 258 | 259 | 1.8 [2022-10-30]: 260 | 1. Improve the support of viewing images inside iframe 261 | 2. Refactor code to tidy up code related to iframe 262 | 263 | 1.7 [2022-10-04]: 264 | 1. Improve support on iframe images 265 | 2. Improve simpleUnlazyImage() 266 | 3. Add more keyboard shortcuts and svg filter in option 267 | 268 | 1.6 [2022-09-03]: 269 | 1. Support images inside iframe 270 | 2. Improve data transfer between content script and background 271 | 272 | 1.5 [2022-08-22]: 273 | 1. Renew simpleUnlazyImage() 274 | 2. Improve image-viewer.js 275 | 3. Support hotkey for reverse search image 276 | 277 | 1.4 [2022-08-10]: 278 | 1. Improve simpleUnlazyImage() 279 | 2. Support video element 280 | 3. Improve MoveTo button logic 281 | 4. Prevent input leak out from image viewer 282 | 5. Improve simpleUnlazyImage() 283 | 6. Add utility.js to separate utility function 284 | 285 | 1.3 [2022-07-01]: 286 | 1. Delay loading of image-viewer.js to improve performance 287 | 2. Add command support 288 | 3. Improve image unlazy 289 | 4. Renew activate image method to increase readability 290 | 291 | 1.2 [2022-07-01]: 292 | 1. Add simpleUnlazyImage() to unlazy image before getting image list 293 | 2. Change CSS to pin image viewer counter 294 | 295 | 1.1 [2022-07-01]: 296 | 1. Support mirror effect 297 | 2. Replace old transform method with matrix to improve performance 298 | 299 | 1.0 [2022-06-29]: 300 | First release on github --------------------------------------------------------------------------------