diff --git a/.gitignore b/.gitignore index 5845df593..25488f8eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,14 @@ node_modules dist dist_dev +dist_dev_safari dist_prod +dist_prod_safari dist_analyze +Timer_Safari_DEV +Timer + firefox_dev market_packages diff --git a/README.md b/README.md index 213cfe0c1..81c428041 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ Timer can [![](https://img.shields.io/amo/rating/2690100?color=green)](https://addons.mozilla.org/en-US/firefox/addon/2690100) [![Mozilla Add-on](https://img.shields.io/amo/users/2690100?color=green)](https://addons.mozilla.org/en-US/firefox/addon/2690100) +[How to install manually for Safari](./doc/safari-install.md) + ![User Count](https://gist.githubusercontent.com/sheepzh/6aaf4c22f909db73b533491167da129b/raw/user_count.svg) ## Contribution diff --git a/doc/dev-guide.md b/doc/dev-guide.md index 447dc0472..67a577965 100644 --- a/doc/dev-guide.md +++ b/doc/dev-guide.md @@ -53,11 +53,25 @@ yarn run test ``` 7. 提交代码,并 PR 主仓库的 main 分支 -## 3. 应用架构设计 +## 3. 在 Safari 里运行 + +1. 重复上述步骤 1-4 +2. 编译兼容 Safari 的代码,替换上述步骤 5 的指令即可 +```shell +npm run dev:safari +``` +3. 使用 Xcode 内置工具 safari-web-extension-converter 将 Chrome 扩展转换成 Safari 扩展 +```shell +[YOUR_PATH]/Xcode.app/Contents/Developer/usr/bin/safari-web-extension-converter dist_dev_safari +``` +项目根目录下会生成一个文件夹 Timer_Safari_DEV,同时 Xcode 会自动打开该文件夹 +4. 在 Xcode 里运行打开的项目即可 + +## 4. 应用架构设计 > todo -## 4. 目录结构 +## 5. 目录结构 ```plain project @@ -117,6 +131,7 @@ project └───webpack # webpack 打包配置 | webpack.common.ts # 基础配置 | webpack.dev.ts # 开发环境配置 + | webpack.dev.safari.ts # Safari 开发环境配置 | webpack.prod.ts # 生产配置 ``` diff --git a/doc/safari-install.md b/doc/safari-install.md new file mode 100644 index 000000000..03c18cf6b --- /dev/null +++ b/doc/safari-install.md @@ -0,0 +1,43 @@ +# How to install for Safari + +This is a too poor developer to pay $99 per year for distribution of an opensource and free browser extension in Apple App Store. +So please intall it **manually**, GG Safari. + +## 0. Download this repository + +```shell +git clone https://github.com/sheepzh/timer.git +cd timer +``` + +## 1. Install tools + +Some tools are required to compile this project to an executable software for Safari. + +* [nodejs & npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) +* [Xcode (compitable for your version of macOS)](https://developer.apple.com/xcode/) + +## 2. Compile source code, install & run + +There are serveral steps. + +1. Compile the sourcecode programmed with TypeScript to js bundles. + +```shell +# Install dependencies +npm install +# Compile +npm run build:safari +``` +Then there will be one folder called **Timer**. + +Also, you can download the archived file from [the release page](https://github.com/sheepzh/timer/releases), and unzip it to gain this folder. + +2. Convert js bundles to Xcode project + +```shell +[YOUR_PATH]/Xcode.app/Contents/Developer/usr/bin/safari-web-extension-converter ./Timer +``` +3. Run Xcode project and one extension app will installed on your macOS +4. Enable this extension +5. Finnally, open your Safari diff --git a/package.json b/package.json index 00a03c57c..7542b3450 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "name": "timer", - "version": "1.2.7", + "version": "1.3.0", "description": "Web timer", "homepage": "https://github.com/sheepzh/timer", "scripts": { "dev": "webpack --config=webpack/webpack.dev.ts --watch", + "dev:safari": "webpack --config=webpack/webpack.dev.safari.ts --watch", "build": "webpack --config=webpack/webpack.prod.ts", + "build:safari": "webpack --config=webpack/webpack.prod.safari.ts", "analyze": "webpack --config=webpack/webpack.analyze.ts", "test": "jest --env=jsdom", "test-c": "jest --coverage --env=jsdom" @@ -56,4 +58,4 @@ "vue": "^3.2.37", "vue-router": "^4.0.16" } -} \ No newline at end of file +} diff --git a/script/data/user_chrome.json b/script/data/user_chrome.json index 43384dfff..3d2fd215a 100644 --- a/script/data/user_chrome.json +++ b/script/data/user_chrome.json @@ -545,5 +545,34 @@ "2022-09-02": 1659, "2022-09-03": 1672, "2022-09-04": 1658, - "2022-09-05": 1679 + "2022-09-05": 1679, + "2022-09-13": 1702, + "2022-09-14": 1716, + "2022-09-15": 1725, + "2022-09-16": 1718, + "2022-09-17": 1716, + "2022-09-18": 1725, + "2022-09-19": 1747, + "2022-09-20": 1749, + "2022-09-21": 1739, + "2022-09-22": 1742, + "2022-09-23": 1754, + "2022-09-24": 1745, + "2022-09-25": 1752, + "2022-09-26": 1753, + "2022-09-27": 1770, + "2022-09-28": 1778, + "2022-09-29": 1794, + "2022-09-30": 1793, + "2022-10-01": 1791, + "2022-10-02": 1789, + "2022-10-03": 1778, + "2022-10-04": 1756, + "2022-10-05": 1726, + "2022-10-06": 1634, + "2022-10-07": 1705, + "2022-10-08": 1755, + "2022-10-09": 1791, + "2022-10-10": 1802, + "2022-10-11": 1822 } \ No newline at end of file diff --git a/script/data/user_edge.json b/script/data/user_edge.json index 8f3577eac..8455f44f2 100644 --- a/script/data/user_edge.json +++ b/script/data/user_edge.json @@ -76,5 +76,9 @@ "2022-08-14": 1636, "2022-08-21": 1649, "2022-08-28": 1641, - "2022-09-05": 1821 + "2022-09-05": 1821, + "2022-09-18": 1851, + "2022-09-25": 1805, + "2022-10-02": 1827, + "2022-10-09": 1925 } \ No newline at end of file diff --git a/script/data/user_firefox.json b/script/data/user_firefox.json index 697c317fc..d87a88972 100644 --- a/script/data/user_firefox.json +++ b/script/data/user_firefox.json @@ -547,5 +547,34 @@ "2022-09-03": 61, "2022-09-04": 54, "2022-09-05": 66, - "2022-09-06": 70 + "2022-09-06": 70, + "2022-09-14": 67, + "2022-09-15": 68, + "2022-09-16": 71, + "2022-09-17": 61, + "2022-09-18": 52, + "2022-09-19": 62, + "2022-09-20": 58, + "2022-09-21": 59, + "2022-09-22": 62, + "2022-09-23": 67, + "2022-09-24": 63, + "2022-09-25": 54, + "2022-09-26": 65, + "2022-09-27": 70, + "2022-09-28": 65, + "2022-09-29": 55, + "2022-09-30": 60, + "2022-10-01": 49, + "2022-10-02": 52, + "2022-10-03": 54, + "2022-10-04": 56, + "2022-10-05": 66, + "2022-10-06": 61, + "2022-10-07": 60, + "2022-10-08": 67, + "2022-10-09": 67, + "2022-10-10": 62, + "2022-10-11": 64, + "2022-10-12": 62 } \ No newline at end of file diff --git a/script/user_count.py b/script/user_count.py index 9352239c8..5081cd303 100644 --- a/script/user_count.py +++ b/script/user_count.py @@ -8,7 +8,7 @@ import math svg_file_path = os.path.join('output', 'user_count.svg') - +output_dir_path = 'output' def smooth_count(last_value, step_num, current_value, data): unit_val = (current_value-last_value) / (step_num+1) @@ -91,6 +91,8 @@ def render(): chart.add('Chrome', chrome_data) chart.add('Edge', edge_data) svg = chart.render() + if not os.path.exists(output_dir_path): + os.makedirs(output_dir_path) with open(svg_file_path, 'wb') as svg_file: svg_file.write(svg) cairosvg.svg2svg(file_obj=open(svg_file_path, 'r'), write_to=svg_file_path) diff --git a/src/app/components/common/host-alert.ts b/src/app/components/common/host-alert.ts index 4c5d85465..14b43e428 100644 --- a/src/app/components/common/host-alert.ts +++ b/src/app/components/common/host-alert.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { IS_SAFARI } from "@util/constant/environment" import { ElLink } from "element-plus" import { computed, ComputedRef, defineComponent, h } from "vue" @@ -39,20 +40,27 @@ const _default = defineComponent({ const href: ComputedRef = computed(() => props.clickable ? `http://${props.host}` : '') const target: ComputedRef = computed(() => props.clickable ? '_blank' : '') const cursor: ComputedRef = computed(() => props.clickable ? "cursor" : "default") - return () => h('div', [ - h(ElLink, - { - href: href.value, - target: target.value, - underline: props.clickable, - style: { cursor: cursor.value } - }, - () => props.host - ), h('span', - { style: HOST_ICON_STYLE }, - h('img', { src: props.iconUrl, width: 12, height: 12 }) - ) - ]) + return IS_SAFARI + ? () => h(ElLink, { + href: href.value, + target: target.value, + underline: props.clickable, + style: { cursor: cursor.value } + }, () => props.host) + : () => h('div', [ + h(ElLink, + { + href: href.value, + target: target.value, + underline: props.clickable, + style: { cursor: cursor.value } + }, + () => props.host + ), h('span', + { style: HOST_ICON_STYLE }, + h('img', { src: props.iconUrl, width: 12, height: 12 }) + ) + ]) } }) diff --git a/src/app/components/option/components/statistics.ts b/src/app/components/option/components/statistics.ts index e6172fd12..f9c901dce 100644 --- a/src/app/components/option/components/statistics.ts +++ b/src/app/components/option/components/statistics.ts @@ -13,6 +13,7 @@ import { defaultStatistics } from "@util/constant/option" import { defineComponent, h, reactive, unref } from "vue" import { t } from "@app/locale" import { renderOptionItem, tagText, tooltip } from "../common" +import { IS_SAFARI } from "@util/constant/environment" function updateOptionVal(key: keyof timer.option.StatisticsOption, newVal: boolean, option: UnwrapRef) { option[key] = newVal @@ -40,6 +41,35 @@ function copy(target: timer.option.StatisticsOption, source: timer.option.Statis target.countLocalFiles = source.countLocalFiles } +function renderOptionItems(option: timer.option.StatisticsOption) { + const result = [] + if (!IS_SAFARI) { + // chrome.idle does not work in Safari, so not to display this option + result.push( + renderOptionItem({ + input: countWhenIdle(option), + idleTime: tagText(msg => msg.option.statistics.idleTime), + info: tooltip(msg => msg.option.statistics.idleTimeInfo) + }, msg => msg.statistics.countWhenIdle, t(msg => msg.option.no)), + h(ElDivider) + ) + } + result.push( + renderOptionItem({ + input: countLocalFiles(option), + localFileTime: tagText(msg => msg.option.statistics.localFileTime), + info: tooltip(msg => msg.option.statistics.localFilesInfo) + }, msg => msg.statistics.countLocalFiles, t(msg => msg.option.no)), + h(ElDivider), + renderOptionItem({ + input: collectSiteName(option), + siteName: tagText(msg => msg.option.statistics.siteName), + siteNameUsage: tooltip(msg => msg.option.statistics.siteNameUsage) + }, msg => msg.statistics.collectSiteName, t(msg => msg.option.yes)) + ) + return result +} + const _default = defineComponent({ name: "StatisticsOptionContainer", setup(_props, ctx) { @@ -51,25 +81,7 @@ const _default = defineComponent({ await optionService.setStatisticsOption(unref(option)) } }) - return () => h('div', [ - renderOptionItem({ - input: countWhenIdle(option), - idleTime: tagText(msg => msg.option.statistics.idleTime), - info: tooltip(msg => msg.option.statistics.idleTimeInfo) - }, msg => msg.statistics.countWhenIdle, t(msg => msg.option.no)), - h(ElDivider), - renderOptionItem({ - input: countLocalFiles(option), - localFileTime: tagText(msg => msg.option.statistics.localFileTime), - info: tooltip(msg => msg.option.statistics.localFilesInfo) - }, msg => msg.statistics.countLocalFiles, t(msg => msg.option.no)), - h(ElDivider), - renderOptionItem({ - input: collectSiteName(option), - siteName: tagText(msg => msg.option.statistics.siteName), - siteNameUsage: tooltip(msg => msg.option.statistics.siteNameUsage) - }, msg => msg.statistics.collectSiteName, t(msg => msg.option.yes)) - ]) + return () => h('div', renderOptionItems(option)) } }) diff --git a/src/app/components/report/index.ts b/src/app/components/report/index.ts index 78d5daf6d..6ad143e7f 100644 --- a/src/app/components/report/index.ts +++ b/src/app/components/report/index.ts @@ -27,6 +27,7 @@ import { useRoute, useRouter } from "vue-router" import { groupBy, sum } from "@util/array" import { formatTime } from "@util/time" import TimerDatabase from "@db/timer-database" +import { IS_SAFARI } from "@util/constant/environment" const timerDatabase = new TimerDatabase(chrome.storage.local) @@ -38,7 +39,7 @@ async function queryData( ) { const loading = ElLoadingService({ target: `.container-card>.el-card__body`, text: "LOADING..." }) const pageInfo = { size: page.size, num: page.num } - const fillFlag = { alias: true, iconUrl: true } + const fillFlag = { alias: true, iconUrl: !IS_SAFARI } const param = { ...queryParam.value, inclusiveRemote: readRemote.value diff --git a/src/background/badge-text-manager.ts b/src/background/badge-text-manager.ts index 4f14db2a5..800b3be84 100644 --- a/src/background/badge-text-manager.ts +++ b/src/background/badge-text-manager.ts @@ -50,16 +50,29 @@ async function updateFocus(host?: string) { if (!host) { host = await findActiveHost() } - const milliseconds = host ? (await timerDb.get(host, new Date)).focus : undefined + const milliseconds = host ? (await timerDb.get(host, new Date())).focus : undefined setBadgeText(milliseconds) } +const ALARM_NAME = 'timer-badge-text-manager-alarm' +const ALARM_INTERVAL = 1000 +function createAlarm(beforeAction?: () => void) { + beforeAction?.() + chrome.alarms.create(ALARM_NAME, { when: Date.now() + ALARM_INTERVAL }) +} + class BadgeTextManager { timer: NodeJS.Timer isPaused: boolean async init() { - this.timer = setInterval(() => !this.isPaused && updateFocus(), 1000) + createAlarm() + chrome.alarms.onAlarm.addListener(alarm => { + if (ALARM_NAME === alarm.name) { + createAlarm(() => !this.isPaused && updateFocus()) + } + }) + // this.timer = setInterval(() => !this.isPaused && updateFocus(), 1000) const option: Partial = await optionService.getAllOption() this.pauseOrResumeAccordingToOption(!!option.displayBadgeText) diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index e62a0809d..a9e062f7e 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2021 Hengyang Zhang * * This software is released under the MIT License. @@ -8,6 +8,7 @@ import { OPTION_ROUTE } from "../app/router/constants" import { getAppPageUrl, SOURCE_CODE_PAGE, TU_CAO_PAGE } from "@util/constant/url" import { t2Chrome } from "@util/i18n/chrome/t" +import { IS_SAFARI } from "@util/constant/environment" const APP_PAGE_URL = getAppPageUrl(true) @@ -19,30 +20,39 @@ const baseProps: Partial = { visible: true } +function titleOf(prefixEmoji: string, title: string) { + if (IS_SAFARI) { + // Emoji does not work in Safari's context menu + return title + } else { + return `${prefixEmoji} ${title}` + } +} + const allFunctionProps: chrome.contextMenus.CreateProperties = { id: chrome.runtime.id + '_timer_menu_item_app_link', - title: '🏷️ ' + t2Chrome(msg => msg.contextMenus.allFunctions), + title: titleOf('🏷️', t2Chrome(msg => msg.contextMenus.allFunctions)), onclick: () => chrome.tabs.create({ url: APP_PAGE_URL }), ...baseProps } const optionPageProps: chrome.contextMenus.CreateProperties = { id: chrome.runtime.id + '_timer_menu_item_option_link', - title: '🥰 ' + t2Chrome(msg => msg.contextMenus.optionPage), + title: titleOf('🥰', t2Chrome(msg => msg.contextMenus.optionPage)), onclick: () => chrome.tabs.create({ url: APP_PAGE_URL + '#' + OPTION_ROUTE }), ...baseProps } const repoPageProps: chrome.contextMenus.CreateProperties = { id: chrome.runtime.id + '_timer_menu_item_repo_link', - title: '🍻 ' + t2Chrome(msg => msg.contextMenus.repoPage), + title: titleOf('🍻', t2Chrome(msg => msg.contextMenus.repoPage)), onclick: () => chrome.tabs.create({ url: SOURCE_CODE_PAGE }), ...baseProps } const feedbackPageProps: chrome.contextMenus.CreateProperties = { id: chrome.runtime.id + '_timer_menu_item_feedback_link', - title: '😿 ' + t2Chrome(msg => msg.contextMenus.feedbackPage), + title: titleOf('😿', t2Chrome(msg => msg.contextMenus.feedbackPage)), onclick: () => chrome.tabs.create({ url: TU_CAO_PAGE }), ...baseProps } diff --git a/src/background/icon-and-alias-collector.ts b/src/background/icon-and-alias-collector.ts index adeec6e2c..499ceae66 100644 --- a/src/background/icon-and-alias-collector.ts +++ b/src/background/icon-and-alias-collector.ts @@ -8,7 +8,7 @@ import HostAliasDatabase from "@db/host-alias-database" import IconUrlDatabase from "@db/icon-url-database" import OptionDatabase from "@db/option-database" -import { IS_CHROME } from "@util/constant/environment" +import { IS_CHROME, IS_SAFARI } from "@util/constant/environment" import { iconUrlOfBrowser } from "@util/constant/url" import { extractHostname, isBrowserUrl, isHomepage } from "@util/pattern" import { defaultStatistics } from "@util/constant/option" @@ -51,7 +51,7 @@ async function processTabInfo(tab: chrome.tabs.Tab): Promise { // localhost hosts with Chrome use cache, so keep the favIcon url undefined IS_CHROME && /^localhost(:.+)?/.test(host) && (favIconUrl = undefined) const iconUrl = favIconUrl || iconUrlOfBrowser(protocol, host) - iconUrlDatabase.put(host, iconUrl) + iconUrl && iconUrlDatabase.put(host, iconUrl) collectAliasEnabled && !isBrowserUrl(url) && isHomepage(url) && collectAlias(host, tab.title) } @@ -67,7 +67,7 @@ function handleWebNavigationCompleted(detail: chrome.webNavigation.WebNavigation } function listen() { - chrome.webNavigation.onCompleted.addListener(handleWebNavigationCompleted) + !IS_SAFARI && chrome.webNavigation.onCompleted.addListener(handleWebNavigationCompleted) } /** diff --git a/src/background/timer/idle-listener.ts b/src/background/timer/idle-listener.ts index ea051644c..0941cfc43 100644 --- a/src/background/timer/idle-listener.ts +++ b/src/background/timer/idle-listener.ts @@ -5,6 +5,7 @@ * https://opensource.org/licenses/MIT */ +import { IS_SAFARI } from "@util/constant/environment" import { formatTime } from "@util/time" import TimerContext from "./context" @@ -24,6 +25,9 @@ export default class IdleListener { } listen() { - chrome.idle.onStateChanged.addListener(newState => listen(this.context, newState)) + if (!IS_SAFARI) { + // Idle does not work in macOs + chrome.idle.onStateChanged.addListener(newState => listen(this.context, newState)) + } } } \ No newline at end of file diff --git a/src/manifest.ts b/src/manifest.ts index b89e86abb..c39e6fe8a 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -34,13 +34,13 @@ const _default: chrome.runtime.ManifestV2 = { }, content_scripts: [ { - "matches": [ + matches: [ "" ], - "js": [ + js: [ "content_scripts.js" ], - "run_at": "document_start" + run_at: "document_start" } ], permissions: [ diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index a6496e2e4..a8750f67c 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -18,6 +18,10 @@ import { formatPeriodCommon, formatTime } from "@util/time" import { t } from "@popup/locale" import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" import { generateSiteLabel } from "@util/site" +import { IS_SAFARI } from "@util/constant/environment" +import { OPTION_ROUTE } from "@app/router/constants" +import { getAppPageUrl } from "@util/constant/url" +import { optionIcon } from "./toolbox-icon" type EcOption = ComposeOption< | PieSeriesOption @@ -75,7 +79,10 @@ function labelFormatter({ mergeHost }: timer.popup.QueryResult, params: any): st const format = params instanceof Array ? params[0] : params const { name } = format const data = format.data as timer.popup.Row - return mergeHost || data.isOther ? name : `{${legend2LabelStyle(name)}|} {a|${name}}` + // Un-supported to get favicon url in Safari + return mergeHost || data.isOther || IS_SAFARI + ? name + : `{${legend2LabelStyle(name)}|} {a|${name}}` } const staticOptions: EcOption = { @@ -114,6 +121,15 @@ const staticOptions: EcOption = { }), excludeComponents: ['toolbox'], pixelRatio: 1 + }, + // Customized tool's name must start with 'my' + myOptions: { + show: true, + title: t(msg => msg.options), + icon: optionIcon, + onclick() { + chrome.tabs.create({ url: getAppPageUrl(false, OPTION_ROUTE, { i: 'popup' }) }) + } } } } diff --git a/src/popup/components/chart/toolbox-icon.ts b/src/popup/components/chart/toolbox-icon.ts new file mode 100644 index 000000000..0adb616cc --- /dev/null +++ b/src/popup/components/chart/toolbox-icon.ts @@ -0,0 +1,2 @@ +export const optionIcon = 'path://' + + 'M499.612903 68.789677L134.35871 279.667613a24.774194 24.774194 0 0 0-12.387097 21.470968v421.739354a24.774194 24.774194 0 0 0 12.387097 21.454452L499.612903 955.210323a24.774194 24.774194 0 0 0 24.774194 0l365.254193-210.877936a24.774194 24.774194 0 0 0 12.387097-21.470968V301.138581a24.774194 24.774194 0 0 0-12.387097-21.454452L524.387097 68.789677a24.774194 24.774194 0 0 0-24.774194 0z m414.802581 167.969033a74.322581 74.322581 0 0 1 37.16129 64.363355v421.75587a74.322581 74.322581 0 0 1-37.16129 64.363355L549.16129 998.119226a74.322581 74.322581 0 0 1-74.32258 0L109.584516 787.24129a74.322581 74.322581 0 0 1-37.16129-64.363355V301.122065a74.322581 74.322581 0 0 1 37.16129-64.363355L474.83871 25.880774a74.322581 74.322581 0 0 1 74.32258 0l365.254194 210.877936zM512 685.419355c-95.777032 0-173.419355-77.642323-173.419355-173.419355S416.222968 338.580645 512 338.580645 685.419355 416.222968 685.419355 512 607.777032 685.419355 512 685.419355z m0-49.548387a123.870968 123.870968 0 1 0 0-247.741936 123.870968 123.870968 0 0 0 0 247.741936z' \ No newline at end of file diff --git a/src/popup/components/footer/index.ts b/src/popup/components/footer/index.ts index 7f15936c4..dc79065fd 100644 --- a/src/popup/components/footer/index.ts +++ b/src/popup/components/footer/index.ts @@ -20,12 +20,13 @@ import { t } from "@popup/locale" import { locale } from "@util/i18n" import { getDayLenth, getMonthTime, getWeekDay, getWeekTime, MILL_PER_DAY } from "@util/time" import optionService from "@service/option-service" +import { IS_SAFARI } from "@util/constant/environment" type FooterParam = TimerQueryParam & { chartTitle: string } -const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: true, alias: true } +const FILL_FLAG_PARAM: FillFlagParam = { iconUrl: !IS_SAFARI, alias: true } function calculateDateRange(duration: timer.popup.Duration, weekStart: timer.option.WeekStartOption): Date | Date[] { const now = new Date() diff --git a/src/popup/locale/messages.ts b/src/popup/locale/messages.ts index 28e35f4cb..4a6b1f7de 100644 --- a/src/popup/locale/messages.ts +++ b/src/popup/locale/messages.ts @@ -17,6 +17,7 @@ export type PopupMessage = { fileName: string saveAsImageTitle: string restoreTitle: string + options: string totalTime: string totalCount: string averageTime: string @@ -43,6 +44,7 @@ const _default: Messages = { fileName: '上网时长清单_{today}_by_{app}', saveAsImageTitle: '保存', restoreTitle: '刷新', + options: '设置', totalTime: '共 {totalTime}', totalCount: '共 {totalCount} 次', averageTime: '平均每天 {value}', @@ -67,6 +69,7 @@ const _default: Messages = { fileName: '上網時長清單_{today}_by_{app}', saveAsImageTitle: '保存', restoreTitle: '刷新', + options: '設置', totalTime: '共 {totalTime}', totalCount: '共 {totalCount} 次', averageCount: '平均每天 {value} 次', @@ -91,6 +94,7 @@ const _default: Messages = { fileName: 'Web_Time_List_{today}_By_{app}', saveAsImageTitle: 'Snapshot', restoreTitle: 'Restore', + options: 'Options', totalTime: 'Total {totalTime}', totalCount: 'Total {totalCount} times', averageCount: '{value} times per day on average', @@ -115,6 +119,7 @@ const _default: Messages = { fileName: 'オンライン時間_{today}_by_{app}', saveAsImageTitle: 'ダウンロード', restoreTitle: '刷新', + options: '設定', totalTime: '合計 {totalTime}', totalCount: '合計 {totalCount} 回', averageTime: '1日平均 {value}', diff --git a/src/util/constant/environment.ts b/src/util/constant/environment.ts index 6c4cc09a2..fd553ea1e 100644 --- a/src/util/constant/environment.ts +++ b/src/util/constant/environment.ts @@ -10,6 +10,7 @@ let isFirefox = false let isChrome = false let isEdge = false let isOpera = false +let isSafari = false if (/Firefox[\/\s](\d+\.\d+)/.test(userAgent)) { isFirefox = true @@ -19,6 +20,9 @@ if (/Firefox[\/\s](\d+\.\d+)/.test(userAgent)) { } else if (userAgent.includes("Opera") || userAgent.includes("OPR")) { // The Opera implements the chrome isOpera = true +} else if (userAgent.includes('Safari') && !userAgent.includes('Chrome')) { + // Chrome on macOs includes 'Safari' + isSafari = true } else if (userAgent.includes('Chrome')) { isChrome = true } @@ -32,4 +36,9 @@ export const IS_CHROME: boolean = isChrome /** * @since 0.8.0 */ -export const IS_OPERA: boolean = isOpera \ No newline at end of file +export const IS_OPERA: boolean = isOpera + +/** + * @since 1.3.0 + */ +export const IS_SAFARI: boolean = isSafari \ No newline at end of file diff --git a/src/util/pattern.ts b/src/util/pattern.ts index 626e2e10d..29282b424 100644 --- a/src/util/pattern.ts +++ b/src/util/pattern.ts @@ -18,6 +18,7 @@ export function isBrowserUrl(url: string) { // Firefox addons' pages || /^moz-extension:/.test(url) || /^edge.*?:\/\/.*$/.test(url) + || /^safari.*?:\/\/.*/.test(url) } const isNotValidPort = (portStr: string) => { diff --git a/webpack/webpack.dev.safari.ts b/webpack/webpack.dev.safari.ts new file mode 100644 index 000000000..6a6d43a0c --- /dev/null +++ b/webpack/webpack.dev.safari.ts @@ -0,0 +1,33 @@ +import path from "path" +import optionGenerator from "./webpack.common" + +const outputDir = path.join(__dirname, '..', 'dist_dev_safari') + +function removeUnsupportedProperties(manifest: Partial) { + // 1. permissions. 'idle' is not supported + const originPermissions = manifest.permissions || [] + const unsupported = ['idle'] + const supported = [] + originPermissions.forEach(perm => !unsupported.includes(perm) && supported.push(perm)) + manifest.permissions = supported +} + +const options = optionGenerator( + outputDir, + baseManifest => { + baseManifest.name = 'Timer_Safari_DEV' + // Remove unsupported properties in Safari + removeUnsupportedProperties(baseManifest) + } +) + +options.mode = 'development' +options.output.path = outputDir + +// no eval with development, but generate *.map.js +options.devtool = 'cheap-module-source-map' + +// Use cache with filesystem +options.cache = { type: 'filesystem' } + +export default options \ No newline at end of file diff --git a/webpack/webpack.dev.ts b/webpack/webpack.dev.ts index 94a17e55d..d5f2cc9b5 100644 --- a/webpack/webpack.dev.ts +++ b/webpack/webpack.dev.ts @@ -56,4 +56,4 @@ options.devtool = 'cheap-module-source-map' // Use cache with filesystem options.cache = { type: 'filesystem' } -module.exports = options \ No newline at end of file +export default options \ No newline at end of file diff --git a/webpack/webpack.prod.safari.ts b/webpack/webpack.prod.safari.ts new file mode 100644 index 000000000..b3ab5cd58 --- /dev/null +++ b/webpack/webpack.prod.safari.ts @@ -0,0 +1,47 @@ +import path from "path" +import optionGenerator from "./webpack.common" +import FileManagerWebpackPlugin from "filemanager-webpack-plugin" +import webpack from "webpack" + +const { name, version } = require(path.join(__dirname, '..', 'package.json')) + +const outputDir = path.join(__dirname, '..', 'dist_prod_safari') +const normalZipFilePath = path.resolve(__dirname, '..', 'market_packages', `${name}-${version}-safari.zip`) + +function removeUnsupportedProperties(manifest: Partial) { + // 1. permissions. 'idle' is not supported + const originPermissions = manifest.permissions || [] + const unsupported = ['idle'] + const supported = [] + originPermissions.forEach(perm => !unsupported.includes(perm) && supported.push(perm)) + manifest.permissions = supported +} + +const options = optionGenerator( + outputDir, + baseManifest => { + baseManifest.name = 'Timer' + // Remove unsupported properties in Safari + removeUnsupportedProperties(baseManifest) + } +) + +const filemanagerWebpackPlugin = new FileManagerWebpackPlugin({ + events: { + // Archive at the end + onEnd: [ + { delete: [path.join(outputDir, '*.LICENSE.txt')] }, + // Define plugin to archive zip for different markets + { + delete: [normalZipFilePath], + archive: [{ source: outputDir, destination: normalZipFilePath }] + } + ] + } +}) + +options.mode = 'production' +options.plugins.push(filemanagerWebpackPlugin as webpack.WebpackPluginInstance) +options.output.path = outputDir + +export default options \ No newline at end of file