diff --git a/README.md b/README.md index 947320ca8..17042beb2 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Timer can - support to set the maximum time for browsing a specific website each day to help you keey away from time wasting. - support to export files formatted _.csv_ and _.json_. - support dark mode. -- support to sync data with Github Gist across serveral browser clients. +- support to sync data with GitHub Gist across serveral browser clients. ## Install diff --git a/package.json b/package.json index d60e82d93..c43177fe7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,11 @@ "test": "jest --env=jsdom", "test-c": "jest --coverage --env=jsdom" }, - "author": "zhy", + "author": { + "name": "zhy", + "email": "returnzhy1996@outlook.com", + "url": "https://www.github.com/sheepzh" + }, "license": "MIT", "devDependencies": { "@crowdin/crowdin-api-client": "^1.22.1", @@ -37,7 +41,7 @@ "node-sass": "^8.0.0", "sass-loader": "^13.2.1", "style-loader": "^3.3.2", - "ts-jest": "^29.0.5", + "ts-jest": "^29.1.0", "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.2", diff --git a/public/images/guide/beating.gif b/public/images/guide/beating.gif new file mode 100644 index 000000000..2749cf82e Binary files /dev/null and b/public/images/guide/beating.gif differ diff --git a/public/images/guide/home.png b/public/images/guide/home.png new file mode 100644 index 000000000..d9bad2587 Binary files /dev/null and b/public/images/guide/home.png differ diff --git a/public/images/guide/pin.png b/public/images/guide/pin.png new file mode 100644 index 000000000..a7fbeec38 Binary files /dev/null and b/public/images/guide/pin.png differ diff --git a/src/api/chrome/runtime.ts b/src/api/chrome/runtime.ts index 918c8a7c0..a728e5976 100644 --- a/src/api/chrome/runtime.ts +++ b/src/api/chrome/runtime.ts @@ -38,4 +38,13 @@ export function getVersion(): string { export function setUninstallURL(url: string): Promise { return new Promise(resolve => chrome.runtime.setUninstallURL(url, resolve)) +} + +/** + * Get the url of this extension + * + * @param path The path relative to the root directory of this extension + */ +export function getUrl(path: string): string { + return chrome.runtime.getURL(path) } \ No newline at end of file diff --git a/src/api/chrome/tab.ts b/src/api/chrome/tab.ts index 7574efded..63a3861a1 100644 --- a/src/api/chrome/tab.ts +++ b/src/api/chrome/tab.ts @@ -1,3 +1,10 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + import { handleError } from "./common" export function getTab(id: number): Promise { @@ -7,6 +14,13 @@ export function getTab(id: number): Promise { })) } +export function getCurrentTab(): Promise { + return new Promise(resolve => chrome.tabs.getCurrent(tab => { + handleError("getCurrentTab") + resolve(tab) + })) +} + export function createTab(param: chrome.tabs.CreateProperties | string): Promise { const prop: chrome.tabs.CreateProperties = typeof param === 'string' ? { url: param } : param return new Promise(resolve => chrome.tabs.create(prop, tab => { @@ -15,6 +29,27 @@ export function createTab(param: chrome.tabs.CreateProperties | string): Promise })) } +/** + * Create one tab after current tab. + * + * Must not be invocked in background.js + */ +export async function createTabAfterCurrent(url: string): Promise { + const tab = await getCurrentTab() + console.log(tab) + if (!tab) { + // Current tab not found + return createTab(url) + } else { + const { windowId, index: currentIndex } = tab + return createTab({ + url, + windowId, + index: (currentIndex ?? -1) + 1 + }) + } +} + export function listTabs(query?: chrome.tabs.QueryInfo): Promise { query = query || {} return new Promise(resolve => chrome.tabs.query(query, tabs => { diff --git a/src/app/components/limit/modify/form/url.ts b/src/app/components/limit/modify/form/url.ts index 9522e3c9e..79b3aabbe 100644 --- a/src/app/components/limit/modify/form/url.ts +++ b/src/app/components/limit/modify/form/url.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -12,6 +12,7 @@ import { ElButton, ElFormItem, ElInput, ElOption, ElSelect } from "element-plus" import { checkPermission, requestPermission } from "@api/chrome/permissions" import { IS_FIREFOX } from "@util/constant/environment" import { parseUrl } from "./common" +import { AUTHOR_EMAIL } from "@src/package" const ALL_PROTOCOLS: Protocol[] = ['http://', 'https://', '*://'] @@ -71,7 +72,7 @@ async function handlePaste(urlHandler: (newUrl: string) => void, protocolHandler } if (!granted) { - alert('Can\'t read the clipboard, please contact the developer via email to returnzhy1996@outlook.com') + alert(`Can\'t read the clipboard, please contact the developer via email to ${AUTHOR_EMAIL}`) return } diff --git a/src/app/components/option/components/backup/index.ts b/src/app/components/option/components/backup/index.ts index 20b3b8a96..ffcee3c0f 100644 --- a/src/app/components/option/components/backup/index.ts +++ b/src/app/components/option/components/backup/index.ts @@ -16,6 +16,7 @@ import { defineComponent, ref, h } from "vue" import { renderOptionItem, tooltip } from "../../common" import BackUpAutoInput from "./auto-input" import Footer from "./footer" +import { AUTHOR_EMAIL } from "@src/package" const ALL_TYPES: timer.backup.Type[] = [ 'none', @@ -128,7 +129,7 @@ const _default = defineComponent({ h(ElAlert, { closable: false, type: "warning", - description: t(msg => msg.option.backup.alert) + description: t(msg => msg.option.backup.alert, { email: AUTHOR_EMAIL }) }), h(ElDivider), renderOptionItem({ diff --git a/src/app/components/rule-merge/components/item.ts b/src/app/components/rule-merge/components/item.ts index 59de3d592..7238606a2 100644 --- a/src/app/components/rule-merge/components/item.ts +++ b/src/app/components/rule-merge/components/item.ts @@ -4,23 +4,16 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { MergeTagType } from "@util/merge" +import type { Ref } from "vue" import { t } from "@app/locale" import { Edit } from "@element-plus/icons-vue" import { tryParseInteger } from "@util/number" import { ElTag } from "element-plus" -import { computed, defineComponent, h, ref, Ref, watch } from "vue" +import { computed, defineComponent, h, ref, watch } from "vue" import ItemInput from "./item-input" - -function computeType(mergedVal: number | string): '' | 'info' | 'success' { - return typeof mergedVal === 'number' ? 'success' : mergedVal === '' ? 'info' : '' -} - -function computeTxt(mergedVal: number | string) { - return typeof mergedVal === 'number' - ? t(msg => msg.mergeRule.resultOfLevel, { level: mergedVal + 1 }) - : mergedVal === '' ? t(msg => msg.mergeRule.resultOfOrigin) : mergedVal -} +import { computeMergeTxt, computeMergeType } from "@util/merge" const _default = defineComponent({ name: "MergeRuleItem", @@ -47,8 +40,10 @@ const _default = defineComponent({ const id: Ref = ref(props.index || 0) watch(() => props.index, newVal => id.value = newVal) const editing: Ref = ref(false) - const type: Ref<'' | 'info' | 'success'> = computed(() => computeType(merged.value)) - const txt: Ref = computed(() => computeTxt(merged.value)) + const type: Ref = computed(() => computeMergeType(merged.value)) + const tagTxt: Ref = computed(() => computeMergeTxt(origin.value, merged.value, + (finder, param) => t(msg => finder(msg.mergeCommon), param) + )) ctx.expose({ forceEdit() { editing.value = true @@ -77,7 +72,10 @@ const _default = defineComponent({ type: type.value, closable: true, onClose: () => ctx.emit("delete", origin.value) - }, () => [`${origin.value} >>> ${txt.value}`, h(Edit, { class: "edit-icon", onclick: () => editing.value = true })]) + }, () => [ + tagTxt.value, + h(Edit, { class: "edit-icon", onclick: () => editing.value = true }) + ]) } }) diff --git a/src/app/layout/menu.ts b/src/app/layout/menu.ts index 624db8210..7b4a08436 100644 --- a/src/app/layout/menu.ts +++ b/src/app/layout/menu.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -7,12 +7,13 @@ import type { UnwrapRef } from "vue" import type ElementIcon from "@src/element-ui/icon" +import type { MenuItemRegistered } from "element-plus" import type { RouteLocationNormalizedLoaded, Router } from "vue-router" import type { I18nKey } from "@app/locale" import type { MenuMessage } from "@i18n/message/app/menu" import { defineComponent, h, onMounted, reactive } from "vue" -import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup, MenuItemRegistered } from "element-plus" +import { ElIcon, ElMenu, ElMenuItem, ElMenuItemGroup } from "element-plus" import { useRoute, useRouter } from "vue-router" import { t } from "@app/locale" import { HOME_PAGE, FEEDBACK_QUESTIONNAIRE, getGuidePageUrl } from "@util/constant/url" @@ -20,7 +21,8 @@ import { Aim, Calendar, ChatSquare, Folder, HelpFilled, HotWater, Memo, Rank, Se import { locale } from "@i18n" import TrendIcon from "./icon/trend-icon" import { createTab } from "@api/chrome/tab" -import { ANALYSIS_ROUTE } from "@app/router/constants" +import { ANALYSIS_ROUTE, MERGE_ROUTE } from "@app/router/constants" +import { START_ROUTE } from "@guide/router/constants" type _MenuItem = { title: keyof MenuMessage @@ -46,7 +48,7 @@ type _RouteProps = { function generateMenus(): _MenuGroup[] { const otherMenuItems: _MenuItem[] = [{ title: 'userManual', - href: getGuidePageUrl(false), + href: getGuidePageUrl(false, START_ROUTE), icon: Memo, index: '_guide', }, { @@ -111,7 +113,7 @@ function generateMenus(): _MenuGroup[] { icon: Tickets }, { title: 'mergeRule', - route: '/additional/rule-merge', + route: MERGE_ROUTE, icon: Rank }, { title: 'option', @@ -135,8 +137,8 @@ function openMenu(route: string, title: I18nKey, routeProps: UnwrapRef<_RoutePro const openHref = (href: string) => createTab(href) -function handleClick(_MenuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { - const { route, title, href } = _MenuItem +function handleClick(menuItem: _MenuItem, routeProps: UnwrapRef<_RouteProps>) { + const { route, title, href } = menuItem if (route) { openMenu(route, msg => msg.menu[title], routeProps) } else { diff --git a/src/app/router/constants.ts b/src/app/router/constants.ts index 242c15216..984beea02 100644 --- a/src/app/router/constants.ts +++ b/src/app/router/constants.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -18,4 +18,9 @@ export const LIMIT_ROUTE = '/behavior/limit' /** * @since 0.9.1 */ -export const REPORT_ROUTE = '/data/report' \ No newline at end of file +export const REPORT_ROUTE = '/data/report' + +/** + * @since 1.8.0 + */ +export const MERGE_ROUTE = '/additional/rule-merge' \ No newline at end of file diff --git a/src/app/router/index.ts b/src/app/router/index.ts index f11dd70e1..38fec026c 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -9,7 +9,7 @@ import type { App } from "vue" import type { RouteRecordRaw } from "vue-router" import { createRouter, createWebHashHistory } from "vue-router" -import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE } from "./constants" +import { OPTION_ROUTE, ANALYSIS_ROUTE, LIMIT_ROUTE, REPORT_ROUTE, MERGE_ROUTE } from "./constants" import metaService from "@service/meta-service" const dataRoutes: RouteRecordRaw[] = [ @@ -58,7 +58,7 @@ const additionalRoutes: RouteRecordRaw[] = [ path: '/additional/whitelist', component: () => import('../components/whitelist') }, { - path: '/additional/rule-merge', + path: MERGE_ROUTE, component: () => import('../components/rule-merge') }, { path: OPTION_ROUTE, diff --git a/src/background/browser-action-menu-manager.ts b/src/background/browser-action-menu-manager.ts index 7e288d679..f3b337148 100644 --- a/src/background/browser-action-menu-manager.ts +++ b/src/background/browser-action-menu-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -13,6 +13,7 @@ import { createTab } from "@api/chrome/tab" import { createContextMenu } from "@api/chrome/context-menu" import { getRuntimeId } from "@api/chrome/runtime" import { locale } from "@i18n" +import { START_ROUTE } from "@guide/router/constants" const APP_PAGE_URL = getAppPageUrl(true) @@ -64,7 +65,7 @@ const feedbackPageProps: ChromeContextMenuCreateProps = { const guidePageProps: ChromeContextMenuCreateProps = { id: getRuntimeId() + '_timer_menu_item_guide_link', title: titleOf('📖', t2Chrome(msg => msg.base.guidePage)), - onclick: () => createTab(getGuidePageUrl(true)), + onclick: () => createTab(getGuidePageUrl(true, START_ROUTE)), ...baseProps } diff --git a/src/element-ui/table.ts b/src/element-ui/table.ts new file mode 100644 index 000000000..3b557895f --- /dev/null +++ b/src/element-ui/table.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { TableColumnCtx } from "element-plus" + +export type ElTableRowScope = { + row: T +} + +export type ElTableSpanMethodProps = { + row: T + column: TableColumnCtx + rowIndex: number + columnIndex: number +} \ No newline at end of file diff --git a/src/guide/component/app.ts b/src/guide/component/app.ts new file mode 100644 index 000000000..e1b960c99 --- /dev/null +++ b/src/guide/component/app.ts @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { MERGE_ROUTE, PRIVACY_ROUTE } from "@guide/router/constants" +import { p, ul, alert } from "./common/util" +import { t } from "@guide/locale" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: PRIVACY_ROUTE, + title: msg => msg.privacy.title, + }, + next: { + route: MERGE_ROUTE, + title: msg => msg.merge.title, + }, + title: msg => msg.app.title, + }, () => [ + p(msg => msg.app.p1), + ul( + [msg => msg.app.l1, { button: t(msg => msg.base.allFunction) }], + [msg => msg.app.l2, { button: t(msg => msg.base.allFunction) }], + ), + alert(msg => msg.app.p2, 'success'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/backup.ts b/src/guide/component/backup.ts new file mode 100644 index 000000000..2da3eb28e --- /dev/null +++ b/src/guide/component/backup.ts @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { LIMIT_ROUTE } from "@guide/router/constants" +import { p, ul, h2, alert, appLink, link } from "./common/util" +import { OPTION_ROUTE, REPORT_ROUTE } from "@app/router/constants" +import { t } from "@guide/locale" +import { ElButton } from "element-plus" +import { UploadFilled } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: LIMIT_ROUTE, + title: msg => msg.limit.title + }, + title: msg => msg.backup.title, + }, () => [ + p(msg => msg.backup.p1, { link: link('https://gist.github.com') }), + h2(msg => msg.backup.upload.title), + ul( + [msg => msg.backup.upload.prepareToken, { link: link('https://github.com/settings/tokens') }], + [msg => msg.backup.upload.enter, { link: appLink(OPTION_ROUTE, { i: 'backup' }) }], + msg => msg.backup.upload.form, + msg => msg.backup.upload.backup, + ), + h2(msg => msg.backup.query.title), + p(msg => msg.backup.query.p1), + ul( + [msg => msg.backup.query.enter, { + link: appLink(REPORT_ROUTE), + menuItem: t(msg => msg.appMenu.dataReport), + }], [msg => msg.backup.query.enable, { + icon: h(ElButton, { + type: 'text', + link: true, + icon: UploadFilled, + disabled: true + }) + }], + msg => msg.backup.query.wait, + ), + alert(msg => msg.backup.query.tip, 'warning'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/common.ts b/src/guide/component/common.ts deleted file mode 100644 index 33e4de794..000000000 --- a/src/guide/component/common.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { I18nKey } from "@guide/locale" -import type { VNode } from "vue" - -import { t, tN } from "@guide/locale" -import { h } from "vue" -import { position2AnchorClz } from "@guide/util" -import { createTab } from "@api/chrome/tab" - -export function h1(i18nKey: I18nKey, position: Position, i18nParam?: any): VNode { - return h('h1', { class: `guide-h1 ${position2AnchorClz(position)}` }, t(i18nKey, i18nParam)) -} - -export function h2(i18nKey: I18nKey, position: Position): VNode { - return h('h2', { class: `guide-h2 ${position2AnchorClz(position)}` }, t(i18nKey)) -} - -export function paragraph(i18nKey: I18nKey, param?: any): VNode { - return h('div', { class: 'guide-paragragh' }, tN(i18nKey, param)) -} - -export function link(href: string, text: string): VNode { - return h('a', { class: 'guide-link', href, target: "_blank" }, text) -} - -export function linkInner(extensionUrl: string, text: string): VNode { - return h('a', { - class: 'guide-link', - onClick: () => createTab(extensionUrl), - }, text) -} - -export function list(...items: (I18nKey | [I18nKey, any])[]): VNode { - const children = items.map(item => { - let param = undefined - let i18nKey: I18nKey = undefined - if (Array.isArray(item)) { - [i18nKey, param] = item - } else { - i18nKey = item - } - return h('li', { class: 'guide-list-item' }, tN(i18nKey, param)) - }) - return h('ul', { class: 'guide-list' }, children) -} - -export function section(...vnodes: VNode[]): VNode { - return h('section', { class: 'guide-area' }, vnodes) -} \ No newline at end of file diff --git a/src/guide/component/common/article.sass b/src/guide/component/common/article.sass new file mode 100644 index 000000000..85b5993dc --- /dev/null +++ b/src/guide/component/common/article.sass @@ -0,0 +1,72 @@ +$footerH: 60px +$titleColor: #222 +$titleColorDark: #DDD +.article-container + margin: 0 15% 50px 15% + font-size: 15px + line-height: 1.5em + font-family: Segoe UI,SegoeUI,Helvetica Neue,Helvetica,Arial,sans-serif + h1, h2 + color: var(--guide-article-title-color) + .article-title + margin-top: 0px + margin-bottom: 32px + font-weight: 600 + font-size: 40px + line-height: 1.2em + .article-content + padding-bottom: 60px + text-align: justify + p, ul + color: var(--el-text-color-primary) + font-size: 16px + line-height: 1.6 + letter-spacing: 0.03em + ul + margin: 16px 0 16px 38px + padding: 0px + li + list-style: disc + h2 + font-size: 28px + line-height: 2 + font-weight: 700 + margin: 1.2em 0 0.8em + .img-container + width: 100% + text-align: center + .el-alert + margin: 30px 0 + padding: 12px 16px + border-radius: 6pxs + .el-alert__icon.is-big + font-size: var(--el-alert-icon-size) + width: var(--el-alert-icon-size) + .el-table__cell + .el-button.is-link + padding: 0 + .article-footer + height: $footerH + display: block + width: 100% + border-top: var(--el-border) + color: var(--el-text-color-primary) + .previous-container,.next-container + height: 100% + display: inline-flex + box-sizing: content-box + font-size: 18px + line-height: $footerH + vertical-align: middle + cursor: pointer + svg + margin: auto 5px + height: 18px + width: 18px + .next-container + float: right + +// Dark Mode +html[data-theme='dark'] + .article-container img + filter: brightness(0.85) saturate(1.25) diff --git a/src/guide/component/common/article.ts b/src/guide/component/common/article.ts new file mode 100644 index 000000000..4d7c936d7 --- /dev/null +++ b/src/guide/component/common/article.ts @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" +import type { PropType } from "vue" +import type { Router } from "vue-router" + +import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue" +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" + +type Link = { + route: string + title: I18nKey +} + +function renderFooter(previous: Link, next: Link, router: Router) { + return h('div', { class: 'article-footer' }, [ + previous && h('div', { + class: 'previous-container', onClick: () => router.push(previous.route) + }, [ + h(ArrowLeft), + h('span', t(previous.title)), + ]), + next && h('div', { class: 'next-container', onClick: () => router.push(next.route) }, [ + h('span', t(next.title)), + h(ArrowRight), + ]) + ]) +} + +const _default = defineComponent({ + props: { + title: { + type: Function as PropType, + required: true, + }, + next: Object as PropType, + previous: Object as PropType, + }, + setup(props, ctx) { + const { previous, next, title } = props + const router = useRouter() + const content = ctx.slots.default + return () => h('div', { class: 'article-container' }, [ + h('h1', { class: 'article-title' }, t(title)), + content && h('div', { class: 'article-content' }, h(content)), + (previous || next) && renderFooter(previous, next, router) + ]) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/common/util.ts b/src/guide/component/common/util.ts new file mode 100644 index 000000000..e02928d76 --- /dev/null +++ b/src/guide/component/common/util.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" +import type { VNode } from "vue" + +import { getUrl } from "@api/chrome/runtime" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { Link } from "@element-plus/icons-vue" +import { t, tN } from "@guide/locale" +import { getAppPageUrl } from "@util/constant/url" +import { ElAlert, ElButton } from "element-plus" +import { h } from "vue" + +/** + * paragraph + */ +export function p(i18nKey: I18nKey, param?: any): VNode { + return h('p', tN(i18nKey, param)) +} + +/** + * Subtitle + */ +export function h2(i18nKey: I18nKey, param?: any): VNode { + return h('h2', tN(i18nKey, param)) +} + +/** + * Image + */ +export function img(fileName: string, param?: { width?: number, height?: number }): VNode { + return h('div', { class: 'img-container' }, + h('img', { ...(param || {}), src: getUrl(`static/images/guide/${fileName}`) }) + ) +} + +/** + * Alert + */ +export function alert(i18nKey: I18nKey, type: 'success' | 'warning' | 'info' = 'info', param?: any): VNode { + return h(ElAlert, { + type, + closable: false, + showIcon: true, + }, () => t(i18nKey, param)) +} + +/** + * ul + */ +export function ul(...liKeys: ([I18nKey, any] | I18nKey)[]): VNode { + return h('ul', { class: 'list-container' }, + liKeys.map(liKey => { + let i18nKey: I18nKey = undefined, param = undefined + if (typeof liKey === 'function') { + i18nKey = liKey + } else { + i18nKey = liKey[0] + param = liKey[1] + } + return h('li', tN(i18nKey, param)) + }) + ) +} + +export const appLink = (route?: string, query?: any) => link(getAppPageUrl(false, route, query)) + +export const link = (url: string) => h(ElButton, { + link: true, + icon: Link, + type: 'primary', + onClick: () => createTabAfterCurrent(url) +}) \ No newline at end of file diff --git a/src/guide/component/home/download-button.ts b/src/guide/component/home/download-button.ts new file mode 100644 index 000000000..13e4bea5b --- /dev/null +++ b/src/guide/component/home/download-button.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { createTabAfterCurrent } from "@api/chrome/tab" +import { ArrowDown, Download } from "@element-plus/icons-vue" +import { t } from "@guide/locale" +import { IS_CHROME, IS_EDGE, IS_FIREFOX } from "@util/constant/environment" +import { CHROME_HOMEPAGE, EDGE_HOMEPAGE, FIREFOX_HOMEPAGE } from "@util/constant/url" +import { ElButton, ElDropdown, ElDropdownItem, ElDropdownMenu, ElIcon } from "element-plus" +import { defineComponent, h } from "vue" + +type _Info = { + name: string + url: string +} + +function allInfo(): _Info[] { + const result: _Info[] = [] + !IS_CHROME && result.push({ name: 'Chrome', url: CHROME_HOMEPAGE }) + !IS_EDGE && result.push({ name: 'Edge', url: EDGE_HOMEPAGE }) + !IS_FIREFOX && result.push({ name: 'Firefox', url: FIREFOX_HOMEPAGE }) + return result +} + +function openUrl(url: string) { + if (!url) return + createTabAfterCurrent(url) +} + +function i18nOfBtn(name: string) { + return t(msg => msg.home.download, { browser: name }) +} + +const _default = defineComponent(() => { + const infos = allInfo() + const firstInfo = infos[0] + const dropdownInfos = infos.slice(1) + return () => h('div', { class: 'download-container' }, [ + firstInfo && h(ElButton, { icon: Download, onClick: () => openUrl(firstInfo.url) }, () => i18nOfBtn(firstInfo.name)), + dropdownInfos?.length && h(ElDropdown, { + onCommand: openUrl, + trigger: 'hover' + }, { + default: () => h(ElIcon, {}, () => h(ArrowDown)), + dropdown: () => h(ElDropdownMenu, {}, () => dropdownInfos + .map(info => h(ElDropdownItem, { command: info.url }, () => i18nOfBtn(info.name))) + ) + }) + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/home/home.sass b/src/guide/component/home/home.sass new file mode 100644 index 000000000..89a867206 --- /dev/null +++ b/src/guide/component/home/home.sass @@ -0,0 +1,45 @@ +.home-container + margin-top: 50px + margin-bottom: 40px + text-align: center + .slogan + font-size: 48px + line-height: 1.25em + font-weight: 600 + margin-bottom: 0.5em + color: var(--el-text-color-primary) + .desc + font-size: 18px + line-height: 1.75em + margin-bottom: 24px + font-weight: 400 + color: var(--el-text-color-regular) + max-width: 55% + margin: 0 auto + padding-bottom: 24px + .button-container + margin-top: 20px + font-size: 16px + font-weight: 400px + .el-button + height: 46px + .download-container + margin-left: 30px + display: inline-block + .el-button + border-top-right-radius: 0 + border-bottom-right-radius: 0 + .el-dropdown + vertical-align: middle + height: 44px + border: var(--el-border) + border-left: none + border-top-right-radius: var(--el-border-radius-base) + border-bottom-right-radius: var(--el-border-radius-base) + background-color: var(--el-border-color) + .el-icon + margin: auto 2px + .el-icon:focus-visible + outline: none + img + width: 23% diff --git a/src/guide/component/home/index.ts b/src/guide/component/home/index.ts new file mode 100644 index 000000000..3dbca1bae --- /dev/null +++ b/src/guide/component/home/index.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import DownloadButton from "./download-button" +import StartButton from "./start-button" +import './home.sass' + +const PIC_URL = chrome.runtime.getURL("static/images/guide/home.png") + +const _default = defineComponent(() => { + return () => h('div', { class: 'home-container' }, [ + h('h1', { class: 'slogan' }, t(msg => msg.meta.slogan)), + h('img', { src: PIC_URL }), + h('p', { class: 'desc' }, t(msg => msg.home.desc)), + h('div', { class: 'button-container' }, [ + h(StartButton), + h(DownloadButton) + ]), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/home/start-button.ts b/src/guide/component/home/start-button.ts new file mode 100644 index 000000000..95293b894 --- /dev/null +++ b/src/guide/component/home/start-button.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { START_ROUTE } from "@guide/router/constants" +import { ElButton } from "element-plus" +import { defineComponent, h } from "vue" +import { useRouter } from "vue-router" +import { Document } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + const router = useRouter() + return () => h(ElButton, { + class: 'start-button', + type: 'primary', + onClick: () => router.push(START_ROUTE), + icon: Document, + }, () => t(msg => msg.home.button)) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/limit.ts b/src/guide/component/limit.ts new file mode 100644 index 000000000..ec4aeaa76 --- /dev/null +++ b/src/guide/component/limit.ts @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { BACKUP_ROUTE, VIRTUAL_ROUTE } from "@guide/router/constants" +import { p, h2, ul, appLink } from "./common/util" +import { t } from "@guide/locale" + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title + }, + next: { + route: BACKUP_ROUTE, + title: msg => msg.backup.title, + }, + title: msg => msg.limit.title, + }, () => [ + p(msg => msg.limit.p1), + h2(msg => msg.limit.step.title), + ul( + [msg => msg.limit.step.enter, { link: appLink(), menuItem: t(msg => msg.appMenu.limit) }], + msg => msg.limit.step.click, + msg => msg.limit.step.form, + msg => msg.limit.step.check, + ), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/merge/index.ts b/src/guide/component/merge/index.ts new file mode 100644 index 000000000..cf53c963c --- /dev/null +++ b/src/guide/component/merge/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "../common/article" +import { APP_PAGE_ROUTE, VIRTUAL_ROUTE } from "@guide/router/constants" +import { p, h2, appLink, } from "../common/util" +import { MERGE_ROUTE } from "@app/router/constants" +import { renderTargetTable, renderSiteExampleTable } from "./target-table" +import { renderSourceTable } from "./source-table" +import { renderRuleTag } from "./rule-tag" +import "./merge.sass" + +const renderDemo = () => renderRuleTag('gist.github.com', 'github.com') + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: APP_PAGE_ROUTE, + title: msg => msg.app.title + }, + next: { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title, + }, + title: msg => msg.merge.title, + }, () => [ + p(msg => msg.merge.p1, { + demo1: h('i', 'www.github.com'), + demo2: h('i', 'gist.github.com'), + }), + p(msg => msg.merge.p2, { link: appLink(MERGE_ROUTE) }), + h2(msg => msg.merge.lookTitle), + p(msg => msg.merge.p3, { demo: renderDemo() }), + h2(msg => msg.merge.source.title), + p(msg => msg.merge.source.p1), + renderSourceTable(), + h2(msg => msg.merge.target.title), + p(msg => msg.merge.target.p1), + renderTargetTable(), + p(msg => msg.merge.target.p2), + renderSiteExampleTable(), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/merge/merge.sass b/src/guide/component/merge/merge.sass new file mode 100644 index 000000000..954eabab7 --- /dev/null +++ b/src/guide/component/merge/merge.sass @@ -0,0 +1,8 @@ +.source-example-cell + display: flex + justify-content: space-between + word-wrap: break-word + .el-tag:last-child + margin-right: auto + .el-tag:not(last-child) + margin-right: 10px diff --git a/src/guide/component/merge/rule-tag.ts b/src/guide/component/merge/rule-tag.ts new file mode 100644 index 000000000..92923eb4e --- /dev/null +++ b/src/guide/component/merge/rule-tag.ts @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { computeMergeTxt, computeMergeType } from "@util/merge" +import { ElTag } from "element-plus" +import { h } from "vue" + +export const renderRuleTag = (origin: string, merged: string | number) => { + const mergedVal = merged ?? '' + return h(ElTag, { + type: computeMergeType(mergedVal), + size: 'small', + }, () => computeMergeTxt(origin, mergedVal, (finder, param) => t(msg => finder(msg.mergeCommon), param))) +} diff --git a/src/guide/component/merge/source-table.ts b/src/guide/component/merge/source-table.ts new file mode 100644 index 000000000..64ff6ca1f --- /dev/null +++ b/src/guide/component/merge/source-table.ts @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { I18nKey } from "@guide/locale" + +import { t, tN } from "@guide/locale" +import { ElTableRowScope } from "@src/element-ui/table" +import { ElTable, ElTableColumn, ElTag } from "element-plus" +import { h } from "vue" + +type _SourceExample = { + source: string + examples: string[] | I18nKey +} + +const renderSiteExample = (site: string) => h(ElTag, { type: 'info', size: 'small' }, () => site) + +const SOURCE_EXAMPLES: _SourceExample[] = [{ + source: 'www.google.com', + examples: msg => msg.merge.source.only, +}, { + source: 'www.google.com.*', + examples: ['www.google.com.hk', 'www.google.com.au'], +}, { + source: '**.mit.edu', + examples: ['www.mit.edu', 'libraries.mit.edu', 'web.mit.edu', 'foo.bar.mit.edu'], +}] + +const renderSourceExample = (row: _SourceExample) => { + const { source, examples } = row + if (typeof examples === 'function') { + return h('span', tN(examples, { source: renderSiteExample(source) })) + } + const exampleTags = examples.map(renderSiteExample) + return h('div', { class: 'source-example-cell' }, exampleTags) +} + +export const renderSourceTable = () => h(ElTable, { + data: SOURCE_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { + label: t(msg => msg.merge.sourceCol), + width: 240, + }, { + default: ({ row }: ElTableRowScope<_SourceExample>) => row.source + }), + h(ElTableColumn, { label: t(msg => msg.merge.source.exampleCol) }, { + default: ({ row }: ElTableRowScope<_SourceExample>) => renderSourceExample(row) + }), +]) \ No newline at end of file diff --git a/src/guide/component/merge/target-table.ts b/src/guide/component/merge/target-table.ts new file mode 100644 index 000000000..de27b18d1 --- /dev/null +++ b/src/guide/component/merge/target-table.ts @@ -0,0 +1,146 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElTableRowScope } from "@src/element-ui/table" +import type { I18nKey } from "@guide/locale" +import type { VNode } from "vue" + +import { t, tN } from "@guide/locale" +import { ElTable, ElTableColumn } from "element-plus" +import { h } from 'vue' +import { renderRuleTag } from "./rule-tag" +import CustomizedHostMergeRuler from "@service/components/host-merge-ruler" +import { PSL_HOMEPAGE } from "@util/constant/url" +import { link } from "../common/util" + +type _RuleExample = { + source: string + target?: string | number +} + +type _TargetExample = _RuleExample & { + remark: I18nKey +} + +type _SiteExample = { + original: string + ruleIdx?: number | number[] + remark?: string | (VNode | string)[] +} + +function computeTarget(val: undefined | string | number): string | number { + return typeof val === 'number' ? val + 1 : (val || '') +} + +const renderTag = (rule: _RuleExample) => renderRuleTag(rule?.source, rule?.target) + +const TARGET_EXAMPLES: _TargetExample[] = [{ + source: 'www.google.com.*', + target: 'google.com', + remark: msg => msg.merge.target.remark.spec, +}, { + source: 'www.google.com.hk', + remark: msg => msg.merge.target.remark.blank +}, { + source: '**.*.google.com', + target: 2, + remark: msg => msg.merge.target.remark.integer +}] + +export const renderTargetTable = () => h(ElTable, { + data: TARGET_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { label: t(msg => msg.merge.sourceCol), width: 180 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => row.source + }), + h(ElTableColumn, { label: t(msg => msg.merge.targetCol), width: 150 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => computeTarget(row.target) + }), + h(ElTableColumn, { label: t(msg => msg.merge.target.lookCol), width: 240 }, { + default: ({ row }: ElTableRowScope<_TargetExample>) => renderTag(row) + }), + h(ElTableColumn, { label: t(msg => msg.merge.remarkCol), minWidth: 300 }, { + default: ({ row: { source, target, remark } }: ElTableRowScope<_TargetExample>) => tN(remark, { + source: h('i', source), + target: h('i', computeTarget(target)), + }) + }), +]) + +const MERGER = new CustomizedHostMergeRuler(TARGET_EXAMPLES.map( + ({ source: origin, target: merged }) => ({ origin, merged }) +)) + +const SITE_EXAMPLES: _SiteExample[] = [{ + original: 'www.google.com.au', + ruleIdx: 0, +}, { + original: 'www.google.com.pt', + ruleIdx: 0, +}, { + original: 'www.google.com.hk', + ruleIdx: [0, 1], + remark: t(msg => msg.merge.target.remark.specFirst), +}, { + original: 'es.news.google.com', + ruleIdx: [2], +}, { + original: 'a.b.c.phontos.google.com', + ruleIdx: [2], +}, { + original: 'pass.hust.edu.cn', + remark: tN(msg => msg.merge.target.remark.miss, { + link: link(PSL_HOMEPAGE), + }), +}] + +function renderHitCell({ ruleIdx }: _SiteExample): string | VNode { + const idxType = typeof ruleIdx + if (idxType === 'undefined') { + return '' + } else if (idxType === 'number') { + const rule = TARGET_EXAMPLES[ruleIdx as number] + return rule ? renderTag(rule) : '' + } else { + return h('span', + (ruleIdx as number[]) + .map(idx => TARGET_EXAMPLES[idx]) + .filter(a => !!a) + .map(renderTag) + ) + } +} + +export const renderSiteExampleTable = () => h(ElTable, { + data: SITE_EXAMPLES, + border: true, + fit: true, +}, () => [ + h(ElTableColumn, { + width: 195, + label: t(msg => msg.merge.target.originalCol), + formatter: (row: _SiteExample) => row.original, + }), + h(ElTableColumn, { + width: 160, + label: t(msg => msg.merge.target.mergedCol), + formatter: (row: _SiteExample) => MERGER.merge(row.original) + }), + h(ElTableColumn, { + width: 235, + label: t(msg => msg.merge.target.hitCol), + }, { + default: ({ row }: ElTableRowScope<_SiteExample>) => renderHitCell(row) + }), + h(ElTableColumn, { + label: t(msg => msg.merge.remarkCol) + }, { + default: ({ row }: ElTableRowScope<_SiteExample>) => row.remark + }) +]) \ No newline at end of file diff --git a/src/guide/component/privacy.ts b/src/guide/component/privacy.ts deleted file mode 100644 index c64545097..000000000 --- a/src/guide/component/privacy.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { defineComponent } from "vue" -import { h1, h2, list, paragraph, section } from "./common" - -const _default = defineComponent({ - setup() { - return () => section( - h1(msg => msg.layout.menu.privacy.title, 'privacy'), - h2(msg => msg.layout.menu.privacy.scope, 'privacy.scope'), - paragraph(msg => msg.privacy.scope.p1), - list( - msg => msg.privacy.scope.l1, - msg => msg.privacy.scope.l2, - msg => msg.privacy.scope.l3, - ), - h2(msg => msg.layout.menu.privacy.storage, 'privacy.storage'), - paragraph(msg => msg.privacy.storage.p1), - paragraph(msg => msg.privacy.storage.p2), - paragraph(msg => msg.privacy.storage.p3), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/privacy/index.ts b/src/guide/component/privacy/index.ts new file mode 100644 index 000000000..022de5095 --- /dev/null +++ b/src/guide/component/privacy/index.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { ElTableRowScope } from "@src/element-ui/table" + +import { defineComponent, h } from "vue" +import Article from "../common/article" +import { p, h2, alert } from "../common/util" +import { ElIcon, ElTable, ElTableColumn } from "element-plus" +import { PrivacyMessage, Scope } from "@i18n/message/guide/privacy" +import { t } from "@guide/locale" +import { CircleCheck, Warning } from "@element-plus/icons-vue" +import './privacy.sass' +import { START_ROUTE, APP_PAGE_ROUTE } from "@guide/router/constants" + +type ScopeRow = keyof PrivacyMessage['scope']['rows'] + +const ALL_ROWS: ScopeRow[] = ['website', 'tab', 'clipboard'] + +const renderTable = () => h(ElTable, { + data: ALL_ROWS, + border: true, + fit: true, + cellClassName: 'scope-table-cell', +}, () => [ + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.name), + minWidth: 200, + }, { + default: ({ row }: ElTableRowScope) => t(msg => msg.privacy.scope.rows[row].name) + }), + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.usage), + minWidth: 500, + }, { + default: ({ row }: ElTableRowScope) => t(msg => msg.privacy.scope.rows[row].usage) + }), + h(ElTableColumn, { + label: t(msg => msg.privacy.scope.cols.required), + width: 250 + }, { + default: ({ row }: ElTableRowScope) => { + const reason = t(msg => (msg.privacy.scope.rows[row] as Scope).optionalReason) + return h('span', { class: reason ? 'optional' : 'required' }, [ + h(ElIcon, () => h(reason ? Warning : CircleCheck)), + reason + ]) + } + }), +]) + +const _default = defineComponent(() => { + return () => h(Article, { + title: msg => msg.privacy.title, + previous: { route: START_ROUTE, title: msg => msg.start.title }, + next: { route: APP_PAGE_ROUTE, title: msg => msg.app.title }, + }, () => [ + alert(msg => msg.privacy.alert, 'warning'), + h2(msg => msg.privacy.scope.title), + renderTable(), + h2(msg => msg.privacy.storage.title), + p(msg => msg.privacy.storage.p1), + p(msg => msg.privacy.storage.p2), + alert(msg => msg.privacy.storage.p3, 'info'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/privacy/privacy.sass b/src/guide/component/privacy/privacy.sass new file mode 100644 index 000000000..1599149b9 --- /dev/null +++ b/src/guide/component/privacy/privacy.sass @@ -0,0 +1,10 @@ +.el-table__row .cell .required svg + color: var(--el-color-success) +.el-table__row .cell .optional svg + color: var(--el-color-warning) + +.el-table__row .cell .el-icon + font-size: 16px + margin-right: 5px +.el-table .cell + word-break: normal !important diff --git a/src/guide/component/profile.ts b/src/guide/component/profile.ts deleted file mode 100644 index 9f1b6651e..000000000 --- a/src/guide/component/profile.ts +++ /dev/null @@ -1,32 +0,0 @@ - -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -import { t } from "@guide/locale" -import { EDGE_HOMEPAGE, CHROME_HOMEPAGE, FIREFOX_HOMEPAGE, SOURCE_CODE_PAGE } from "@util/constant/url" -import { defineComponent } from "vue" - -import { h1, paragraph, link, section } from "./common" - -const _default = defineComponent({ - name: 'GuideProfile', - setup() { - return () => section( - h1(msg => msg.layout.menu.profile, 'profile', { appName: t(msg => msg.meta.name) }), - paragraph(msg => msg.profile.p1, { - edge: link(EDGE_HOMEPAGE, 'Edge'), - chrome: link(CHROME_HOMEPAGE, 'Chrome'), - firefox: link(FIREFOX_HOMEPAGE, 'Firefox'), - github: link(SOURCE_CODE_PAGE, 'Github'), - appName: t(msg => msg.meta.name), - }), - paragraph(msg => msg.profile.p2), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/start.ts b/src/guide/component/start.ts new file mode 100644 index 000000000..4503ac286 --- /dev/null +++ b/src/guide/component/start.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { img, p, h2, alert } from "./common/util" +import { getUrl } from "@api/chrome/runtime" +import { PRIVACY_ROUTE } from "@guide/router/constants" + +const _default = defineComponent(() => { + return () => h(Article, { + title: msg => msg.start.title, + next: { title: msg => msg.privacy.title, route: PRIVACY_ROUTE }, + }, () => [ + p(msg => msg.start.p1), + h2(msg => msg.start.s1), + p(msg => msg.start.s1p1), + img('pin.png', { height: 300 }), + h2(msg => msg.start.s2), + p(msg => msg.start.s2p1, { + demo: h('img', { src: getUrl('static/images/guide/beating.gif') }) + }), + h2(msg => msg.start.s3), + p(msg => msg.start.s3p1), + alert(msg => msg.start.alert, 'success'), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/component/usage.ts b/src/guide/component/usage.ts deleted file mode 100644 index b634e1032..000000000 --- a/src/guide/component/usage.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getAppPageUrl, PSL_HOMEPAGE } from "@util/constant/url" -import { defineComponent } from "vue" -import { h1, h2, paragraph, list, link, section, linkInner } from "./common" -import { t } from "../locale" - -const quickstart = () => [ - h2(msg => msg.layout.menu.usage.quickstart, 'usage.quickstart'), - paragraph(msg => msg.usage.quickstart.p1), - list( - msg => msg.usage.quickstart.l1, - msg => msg.usage.quickstart.l2, - msg => msg.usage.quickstart.l3, - ), - paragraph(msg => msg.usage.quickstart.p2), -] - -const backgroundPageUrl = getAppPageUrl(false) - -const background = () => [ - h2(msg => msg.layout.menu.usage.background, 'usage.background'), - paragraph(msg => msg.usage.background.p1, { - background: linkInner(backgroundPageUrl, t(msg => msg.usage.background.backgroundPage)) - }), - list( - [msg => msg.usage.background.l1, { allFunction: t(msg => msg.base.allFunction) }], - [msg => msg.usage.background.l2, { allFunction: t(msg => msg.base.allFunction) }], - ), - paragraph(msg => msg.usage.background.p2), -] - -const advanced = () => [ - h2(msg => msg.layout.menu.usage.advanced, 'usage.advanced'), - paragraph(msg => msg.usage.advanced.p1), - list( - msg => msg.usage.advanced.l1, - msg => msg.usage.advanced.l2, - msg => msg.usage.advanced.l3, - msg => msg.usage.advanced.l4, - [msg => msg.usage.advanced.l5, { - psl: link(PSL_HOMEPAGE, 'Public Suffix List') - }], - msg => msg.usage.advanced.l6, - msg => msg.usage.advanced.l7, - msg => msg.usage.advanced.l8, - ), -] - -const backup = () => [ - h2(msg => msg.layout.menu.usage.backup, 'usage.backup'), - paragraph(msg => msg.usage.backup.p1, { gist: link('https://gist.github.com/', 'Github Gist') }), - list( - [msg => msg.usage.backup.l1, { - token: link('https://github.com/settings/tokens', 'token') - }], - msg => msg.usage.backup.l2, - msg => msg.usage.backup.l3, - ), -] - -const _default = defineComponent({ - setup() { - return () => section( - h1(msg => msg.layout.menu.usage.title, 'usage'), - ...quickstart(), - ...background(), - ...advanced(), - ...backup(), - ) - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/component/virtual.ts b/src/guide/component/virtual.ts new file mode 100644 index 000000000..014afc5e4 --- /dev/null +++ b/src/guide/component/virtual.ts @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" +import Article from "./common/article" +import { LIMIT_ROUTE, MERGE_ROUTE } from "@guide/router/constants" +import { p, h2, ul, appLink } from "./common/util" +import { ElTag } from "element-plus" +import { t } from "@guide/locale" + +const demoTag = (demoSite: string) => h(ElTag, { size: 'small', type: 'info' }, () => demoSite) + +const _default = defineComponent(() => { + return () => h(Article, { + previous: { + route: MERGE_ROUTE, + title: msg => msg.merge.title + }, + next: { + route: LIMIT_ROUTE, + title: msg => msg.limit.title + }, + title: msg => msg.virtual.title, + }, () => [ + p(msg => msg.virtual.p1), + h2(msg => msg.virtual.step.title), + ul( + [msg => msg.virtual.step.enter, { link: appLink(), menuItem: t(msg => msg.appMenu.siteManage) }], + msg => msg.virtual.step.click, + [msg => msg.virtual.step.form, { + demo1: demoTag('github.com/sheepzh'), + demo2: demoTag('github.com/sheepzh/timer/**'), + }], + msg => msg.virtual.step.browse, + ), + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/guide.d.ts b/src/guide/guide.d.ts deleted file mode 100644 index a14023b1c..000000000 --- a/src/guide/guide.d.ts +++ /dev/null @@ -1,14 +0,0 @@ - -/** - * The anchor of menu items - */ -declare type Position = - | 'profile' - | 'usage' - | 'usage.quickstart' - | 'usage.background' - | 'usage.advanced' - | 'usage.backup' - | 'privacy' - | 'privacy.scope' - | 'privacy.storage' \ No newline at end of file diff --git a/src/guide/index.ts b/src/guide/index.ts index 3334af4c8..839e4808b 100644 --- a/src/guide/index.ts +++ b/src/guide/index.ts @@ -1,30 +1,35 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ +import type { Language } from "element-plus/lib/locale" import "./style" -import 'element-plus/theme-chalk/index.css' - -import { initLocale } from "@i18n" +import ElementPlus from 'element-plus' +import { initLocale, locale } from "@i18n" import { t } from "./locale" -import { init as initTheme, toggle } from "@util/dark-mode" +import installRouter from "./router" import { createApp } from "vue" import Main from "./layout" -import optionService from "@service/option-service" +import 'element-plus/theme-chalk/index.css' + +const locales: { [locale in timer.Locale]: () => Promise<{ default: Language }> } = { + zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), + zh_TW: () => import('element-plus/lib/locale/lang/zh-tw'), + en: () => import('element-plus/lib/locale/lang/en'), + ja: () => import('element-plus/lib/locale/lang/ja') +} async function main() { - initTheme() - // Calculate the latest mode - optionService.isDarkMode().then(toggle) await initLocale() - const app = createApp(Main) - app.mount('#guide') + installRouter(app) document.title = t(msg => msg.base.guidePage) + ' | ' + t(msg => msg.meta.name) + locales[locale]?.()?.then(msg => app.use(ElementPlus, { locale: msg.default })) + app.mount('#guide') } main() diff --git a/src/guide/layout/content.ts b/src/guide/layout/content.ts deleted file mode 100644 index 91217a2f1..000000000 --- a/src/guide/layout/content.ts +++ /dev/null @@ -1,34 +0,0 @@ - -import { ElDivider } from "element-plus" -import { watch, defineComponent, h, onMounted, PropType } from "vue" -import Profile from "../component/profile" -import Usage from "../component/usage" -import Privacy from "../component/privacy" -import { position2AnchorClz } from "@guide/util" - -function scrollPosition(position: Position) { - document.querySelector(`.${position2AnchorClz(position)}`)?.scrollIntoView?.() -} - -const _default = defineComponent({ - name: 'GuideContent', - props: { - position: { - type: String as PropType, - required: false, - } - }, - setup(props) { - onMounted(() => scrollPosition(props.position)) - watch(() => props.position, newVal => newVal && scrollPosition(newVal)) - return () => [ - h(Profile), - h(ElDivider), - h(Usage), - h(ElDivider), - h(Privacy), - ] - } -}) - -export default _default \ No newline at end of file diff --git a/src/guide/layout/header/dark-switch.ts b/src/guide/layout/header/dark-switch.ts new file mode 100644 index 000000000..47ea08aed --- /dev/null +++ b/src/guide/layout/header/dark-switch.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { Ref } from "vue" + +import { defineComponent, ref, h } from "vue" +import { init as initTheme, toggle } from "@util/dark-mode" +import optionService from "@service/option-service" +import { ElSwitch } from "element-plus" +import { Moon, Sunrise } from "@element-plus/icons-vue" + +const _default = defineComponent(() => { + const value: Ref = ref(initTheme()) + const handleChange = (newVal: boolean) => { + toggle(newVal) + value.value = newVal + } + // Calculate the latest mode + optionService.isDarkMode().then(handleChange) + return () => h(ElSwitch, { + modelValue: value.value, + inactiveIcon: Sunrise, + activeIcon: Moon, + inlinePrompt: true, + async onChange(newVal: boolean) { + handleChange(newVal) + const option = await optionService.getAllOption() + option.darkMode = newVal ? 'on' : 'off' + optionService.setAppearanceOption(option) + }, + }) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/header.sass b/src/guide/layout/header/header.sass new file mode 100644 index 000000000..c685da29e --- /dev/null +++ b/src/guide/layout/header/header.sass @@ -0,0 +1,71 @@ + +/** + * Copyright (c) 2023 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +$logoSize: 42px +$iconSize: 24px +$localeIconSize: 19px + +.header-container + line-height: 60px + height: 60px + display: flex + justify-content: space-between + margin: 0 auto + .header-left + display: flex + box-sizing: content-box + cursor: pointer + .icon,.title + height: $logoSize + line-height: $logoSize + display: inline-block + margin: auto 0 + .icon + margin-right: 10px + width: $logoSize + .title + font-size: 20px + color: var(--el-text-color-primary) + .header-right + .el-switch__inner + svg path + fill: var(--el-color-white) + svg + width: $iconSize + height: $iconSize + path + fill: var(--el-text-color-regular) + .el-switch + --el-switch-off-color: var(--el-color-primary) + // = [light-theme] --el-text-color-regular + --el-switch-on-color: #606266 + margin-left: 10px + .el-switch__core + width: 45px + .icon-link + display: inline-flex + background-color: transparent + height: $iconSize + border: none + padding: 0px + .locale-select, .icon-link + vertical-align: middle + margin-left: 10px + .locale-select + .locale-button + display: flex + svg + width: $localeIconSize + height: $localeIconSize + span + font-size: calc( $localeIconSize - 3 ) + padding-left: 3px + line-height: $localeIconSize + color: var(--el-text-color-regular) + .locale-button:focus-visible + outline: none diff --git a/src/guide/layout/header/icon-button.ts b/src/guide/layout/header/icon-button.ts new file mode 100644 index 000000000..d6c3c8654 --- /dev/null +++ b/src/guide/layout/header/icon-button.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElTooltip } from "element-plus" +import { defineComponent, h } from "vue" +import SvgIcon from "./svg-icon" + +const _default = defineComponent({ + props: { + path: String, + tip: String, + href: String, + }, + setup(props) { + return () => h(ElTooltip, { + effect: 'dark', + placement: 'bottom', + content: props.tip, + offset: 5, + showArrow: false, + }, () => h('a', { + href: props.href, + target: '_blank', + class: 'icon-link', + }, h(SvgIcon, { path: props.path }))) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/index.ts b/src/guide/layout/header/index.ts new file mode 100644 index 000000000..abcb1cc48 --- /dev/null +++ b/src/guide/layout/header/index.ts @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { t } from "@guide/locale" +import { defineComponent, h } from "vue" +import IconButton from "./icon-button" +import DarkSwitch from "./dark-switch" +import LocaleSelect from "./locale-select" +import './header.sass' +import { GITHUB_PATH, EMAIL_PATH } from "./svg" +import { useRouter } from "vue-router" +import { HOME_ROUTE } from "@guide/router/constants" +import { SOURCE_CODE_PAGE } from "@util/constant/url" +import { AUTHOR_EMAIL } from "@src/package" + +const ICON_URL = chrome.runtime.getURL("static/images/icon.png") + +const logo = () => h('span', { class: 'icon' }, h('img', { src: ICON_URL, width: 42, height: 42 })) +const title = () => h('span', { class: 'title' }, t(msg => msg.meta.marketName)) +const github = () => h(IconButton, { + path: GITHUB_PATH, + tip: t(msg => msg.layout.header.sourceCode), + href: SOURCE_CODE_PAGE, +}) +const email = () => h(IconButton, { + path: EMAIL_PATH, + tip: t(msg => msg.layout.header.email), + href: `mailto:${AUTHOR_EMAIL}` +}) +const darkSwitch = () => h(DarkSwitch) +const localeSelect = () => h(LocaleSelect) + +const _default = defineComponent(() => { + const router = useRouter() + return () => h('div', { class: 'header-container' }, [ + h('div', { class: 'header-left', onClick: () => router.push(HOME_ROUTE) }, [logo(), title()]), + h('div', { class: 'header-right' }, [ + github(), + email(), + darkSwitch(), + localeSelect(), + ]) + ]) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/locale-select.ts b/src/guide/layout/header/locale-select.ts new file mode 100644 index 000000000..d7cb5e97e --- /dev/null +++ b/src/guide/layout/header/locale-select.ts @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { ElDropdown, ElDropdownItem, ElDropdownMenu } from "element-plus" +import { defineComponent, h } from "vue" +import { getI18nVal, locale as current } from "@i18n" +import localeMessages from "@i18n/message/common/locale" +import SvgIcon from "./svg-icon" +import { LOCALE_PATH } from "./svg" +import optionService from "@service/option-service" +import { createTabAfterCurrent } from "@api/chrome/tab" +import { CROWDIN_HOMEPAGE } from "@util/constant/url" + +const HELP_CMD: string = '_help' +const ALL_LOCALES: timer.Locale[] = ['en', 'zh_CN', 'zh_TW', 'ja'] + +const getLocaleName = (locale: timer.Locale) => getI18nVal(localeMessages, msg => msg.name, locale) + +const renderIcon = () => h('div', + { class: 'locale-button' }, + [ + h(SvgIcon, { path: LOCALE_PATH }), + h('span', getLocaleName(current)) + ] +) + +const renderLocaleItem = (locale: timer.Locale) => h(ElDropdownItem, { + disabled: current === locale, + command: locale, +}, () => getLocaleName(locale)) + +const renderHelpItem = () => h(ElDropdownItem, { + divided: true, + command: HELP_CMD, +}, () => 'Help translate!') + +const renderMenuItems = () => h(ElDropdownMenu, + () => [ + ...ALL_LOCALES.map(renderLocaleItem), + renderHelpItem(), + ] +) + +const handleCommand = async (cmd: string) => { + if (cmd === HELP_CMD) { + createTabAfterCurrent(CROWDIN_HOMEPAGE) + return + } + const locale = cmd as timer.Locale + const option = await optionService.getAllOption() + option.locale = locale + await optionService.setAppearanceOption(option) + window.location.reload?.() +} + +const _default = defineComponent(() => { + return () => h(ElDropdown, { + class: 'locale-select', + onCommand: handleCommand + }, { + default: () => renderIcon(), + dropdown: () => renderMenuItems() + }) +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/svg-icon.ts b/src/guide/layout/header/svg-icon.ts new file mode 100644 index 000000000..2511e6e52 --- /dev/null +++ b/src/guide/layout/header/svg-icon.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { defineComponent, h } from "vue" + +const _default = defineComponent({ + props: { + path: String, + }, + setup(props) { + return () => h('svg', { + viewBox: "0 0 1024 1024", + xmlns: "http://www.w3.org/2000/svg" + }, h("path", { d: props.path })) + } +}) + +export default _default \ No newline at end of file diff --git a/src/guide/layout/header/svg.ts b/src/guide/layout/header/svg.ts new file mode 100644 index 000000000..4fa03b3ff --- /dev/null +++ b/src/guide/layout/header/svg.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export const GITHUB_PATH = 'M511.6 76.3C264.3 76.2 64 276.4 64 523.5 64 718.9 189.3 885 363.8 946c23.5 5.9 19.9-10.8 19.9-22.2v-77.5c-135.7 15.9-141.2-73.9-150.3-88.9C215 726 171.5 718 184.5 703c30.9-15.9 62.4 4 98.9 57.9 26.4 39.1 77.9 32.5 104 26 5.7-23.5 17.9-44.5 34.7-60.8-140.6-25.2-199.2-111-199.2-213 0-49.5 16.3-95 48.3-131.7-20.4-60.5 1.9-112.3 4.9-120 58.1-5.2 118.5 41.6 123.2 45.3 33-8.9 70.7-13.6 112.9-13.6 42.4 0 80.2 4.9 113.5 13.9 11.3-8.6 67.3-48.8 121.3-43.9 2.9 7.7 24.7 58.3 5.5 118 32.4 36.8 48.9 82.7 48.9 132.3 0 102.2-59 188.1-200 212.9 23.5 23.2 38.1 55.4 38.1 91v112.5c0.8 9 0 17.9 15 17.9 177.1-59.7 304.6-227 304.6-424.1 0-247.2-200.4-447.3-447.5-447.3z' +export const EMAIL_PATH = 'M853.333333 170.666667H170.666667c-47.146667 0-84.906667 38.186667-84.906667 85.333333L85.333333 768c0 47.146667 38.186667 85.333333 85.333334 85.333333h682.666666c47.146667 0 85.333333-38.186667 85.333334-85.333333V256c0-47.146667-38.186667-85.333333-85.333334-85.333333z m0 170.666666L512 554.666667 170.666667 341.333333v-85.333333l341.333333 213.333333 341.333333-213.333333v85.333333z' +export const LOCALE_PATH = 'M540.245333 645.888L426.410667 522.069333l1.365333-1.536a889.770667 889.770667 0 0 0 166.314667-322.048h131.413333V99.669333H411.562667V1.109333h-89.6v98.645334H7.850667v98.730666h500.906666a797.696 797.696 0 0 1-142.165333 263.936 778.581333 778.581333 0 0 1-103.594667-165.290666h-89.6c33.621333 82.176 78.677333 158.037333 133.546667 224.938666L78.848 769.706667l63.658667 69.973333 224.170666-246.613333 139.52 153.429333 34.133334-100.608z m252.501334-250.026667H703.061333l-201.813333 591.872h89.685333l50.261334-147.968h212.992l50.688 147.968h89.685333L792.746667 395.776zM675.242667 741.034667l72.618666-213.674667 72.704 213.674667H675.242667z' diff --git a/src/guide/layout/index.ts b/src/guide/layout/index.ts index 7cec2f643..fa3ab421b 100644 --- a/src/guide/layout/index.ts +++ b/src/guide/layout/index.ts @@ -1,28 +1,28 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { Ref } from "vue" - -import { ElContainer, ElAside, ElMain } from "element-plus" -import { defineComponent, ref, h } from "vue" -import Content from "./content" +import { ElContainer, ElAside, ElMain, ElHeader, ElScrollbar } from "element-plus" +import { defineComponent, h } from "vue" +import Header from "./header" import Menu from "./menu" +import { RouterView, useRoute } from "vue-router" +import { HOME_ROUTE } from "@guide/router/constants" + +const renderMain = () => h(ElContainer, {}, () => [ + h(ElAside, () => h(ElScrollbar, () => h(Menu))), + h(ElMain, () => h(ElScrollbar, () => h(RouterView))), +]) -const _default = defineComponent({ - name: "Guide", - render() { - const position: Ref = ref() - return h(ElContainer, { class: 'guide-container' }, () => [ - h(ElAside, {}, () => h(Menu, { onClick: (newPosition: Position) => position.value = newPosition })), - h(ElContainer, { - id: 'app-body' - }, () => h(ElMain, {}, () => h(Content, { position: position.value }))) - ]) - } +const _default = defineComponent(() => { + const route = useRoute() + return () => h(ElContainer, { class: 'guide-container' }, () => [ + h(ElHeader, () => h(Header)), + route.path === HOME_ROUTE ? h(RouterView) : renderMain(), + ]) }) export default _default \ No newline at end of file diff --git a/src/guide/layout/menu.ts b/src/guide/layout/menu.ts index 21db6fd1c..1688faae2 100644 --- a/src/guide/layout/menu.ts +++ b/src/guide/layout/menu.ts @@ -5,118 +5,87 @@ * https://opensource.org/licenses/MIT */ -import { ref, VNode } from "vue" -import ElementIcon from "@src/element-ui/icon" -import { I18nKey, t } from "@guide/locale" -import { ElIcon, ElMenu, ElMenuItem, ElSubMenu } from "element-plus" -import { defineComponent, h } from "vue" -import { User, Memo, MagicStick } from "@element-plus/icons-vue" +import type { Ref } from "vue" +import type { I18nKey } from "@guide/locale" -type _Item = { - title: I18nKey - position: Position -} +import { t } from "@guide/locale" +import { ElMenu, ElMenuItem, ElSubMenu } from "element-plus" +import { defineComponent, h, onMounted, ref } from "vue" +import { Router, useRouter } from "vue-router" +import { + START_ROUTE, + PRIVACY_ROUTE, + USAGE_ROUTE, + APP_PAGE_ROUTE, + MERGE_ROUTE, + VIRTUAL_ROUTE, + BACKUP_ROUTE, + LIMIT_ROUTE, +} from "@guide/router/constants" -type _Group = { +type MenuConf = { + route: string title: I18nKey - position: Position - children: _Item[] - icon: ElementIcon + children?: MenuConf[] } -const quickstartPosition: Position = 'usage.quickstart' -const profilePosition: Position = 'profile' -const menus: _Group[] = [ - { - title: msg => msg.layout.menu.usage.title, - position: 'usage', - children: [ - { - title: msg => msg.layout.menu.usage.quickstart, - position: quickstartPosition - }, { - title: msg => msg.layout.menu.usage.background, - position: 'usage.background' - }, { - title: msg => msg.layout.menu.usage.advanced, - position: 'usage.advanced' - }, { - title: msg => msg.layout.menu.usage.backup, - position: 'usage.backup', - } - ], - icon: Memo - }, - { - title: msg => msg.layout.menu.privacy.title, - position: 'privacy', - icon: User, - children: [ - { - title: msg => msg.layout.menu.privacy.scope, - position: 'privacy.scope' - }, - { - title: msg => msg.layout.menu.privacy.storage, - position: 'privacy.storage' - }, - ], +const MENU_CONFS: MenuConf[] = [{ + route: START_ROUTE, + title: msg => msg.start.title, +}, { + route: PRIVACY_ROUTE, + title: msg => msg.privacy.title, +}, { + route: USAGE_ROUTE, + title: msg => msg.layout.menu.usage, + children: [{ + route: APP_PAGE_ROUTE, + title: msg => msg.app.title, + }, { + route: MERGE_ROUTE, + title: msg => msg.merge.title, + }, { + route: VIRTUAL_ROUTE, + title: msg => msg.virtual.title, + }, { + route: LIMIT_ROUTE, + title: msg => msg.limit.title, + }, { + route: BACKUP_ROUTE, + title: msg => msg.backup.title + }], +}] +function renderWithConf(conf: MenuConf, router: Router, activeRoute: Ref) { + const { route, title, children } = conf + if (children?.length) { + return h(ElSubMenu, { + index: route, + }, { + title: () => h('span', t(title)), + default: () => children.map(child => renderWithConf(child, router, activeRoute)) + }) } -] - -function renderMenuItem(handleClick: (position: Position) => void, item: _Item, index: number): VNode { - const { title, position } = item return h(ElMenuItem, { - index: position, - onClick: () => handleClick(position) - }, () => h('span', {}, `${index + 1}. ${t(title)}`)) + index: route, + onClick: () => { + router.push(route) + activeRoute.value = route + }, + }, () => h('span', t(title))) } -function renderGroup(handleClick: (position: Position) => void, group: _Group): VNode { - const { position, title, children, icon } = group - return h(ElSubMenu, { - index: position, - }, { - title: () => [ - h(ElIcon, () => h(icon)), - h('span', {}, t(title)) - ], - default: () => children.map( - (item, index) => renderMenuItem(handleClick, item, index) - ) - }) -} - -const _default = defineComponent({ - name: "GuideMenu", - emits: { - click: (_position: Position) => true, - }, - setup(_, ctx) { - const handleClick = (position: Position) => ctx.emit('click', position) - const menuItems = () => [ - h(ElMenuItem, { - index: profilePosition, - onClick: () => handleClick(profilePosition) - }, () => [ - h(ElIcon, () => h(MagicStick)), - h('span', {}, t(msg => msg.layout.menu.profile, { appName: t(msg => msg.meta.name) })) - ]), - ...menus.map( - group => renderGroup(handleClick, group) - ) - ] - const menuRef = ref() - return () => h(ElMenu, { - defaultActive: profilePosition, - defaultOpeneds: menus.map(group => group.position), - ref: menuRef, - onClose(index: string) { - menuRef.value?.open?.(index) - } - }, menuItems) - } +const _default = defineComponent(() => { + const router = useRouter() + const activeRoute: Ref = ref() + // Initialize current route in a new macro task + onMounted(() => setTimeout(() => activeRoute.value = router.currentRoute?.value?.path)) + return () => [ + h(ElMenu, { + defaultOpeneds: MENU_CONFS.filter(m => m.children?.length).map(group => group.route), + defaultActive: activeRoute.value, + }, () => MENU_CONFS.map(conf => renderWithConf(conf, router, activeRoute))) + ] }) export default _default \ No newline at end of file diff --git a/src/guide/router/constants.ts b/src/guide/router/constants.ts new file mode 100644 index 000000000..484151ba8 --- /dev/null +++ b/src/guide/router/constants.ts @@ -0,0 +1,16 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export const HOME_ROUTE = '/home' +export const START_ROUTE = '/start' +export const PRIVACY_ROUTE = '/privacy' +export const USAGE_ROUTE = '/usage' +export const APP_PAGE_ROUTE = '/usage/management' +export const MERGE_ROUTE = '/usage/merge' +export const VIRTUAL_ROUTE = '/usage/virtual' +export const LIMIT_ROUTE = '/usage/limit' +export const BACKUP_ROUTE = '/usage/backup' \ No newline at end of file diff --git a/src/guide/router/index.ts b/src/guide/router/index.ts new file mode 100644 index 000000000..0c248b1cd --- /dev/null +++ b/src/guide/router/index.ts @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { RouteRecordRaw } from "vue-router" + +import { App } from "vue" +import { createRouter, createWebHashHistory } from "vue-router" +import { + HOME_ROUTE, + START_ROUTE, + PRIVACY_ROUTE, + USAGE_ROUTE, + APP_PAGE_ROUTE, + MERGE_ROUTE, + VIRTUAL_ROUTE, + LIMIT_ROUTE, + BACKUP_ROUTE, +} from "./constants" + + +const routes: RouteRecordRaw[] = [{ + path: '/', + redirect: HOME_ROUTE, +}, { + path: HOME_ROUTE, + component: () => import('../component/home'), +}, { + path: START_ROUTE, + component: () => import('../component/start'), +}, { + path: PRIVACY_ROUTE, + component: () => import('../component/privacy') +}, { + path: USAGE_ROUTE, + redirect: APP_PAGE_ROUTE, +}, { + path: APP_PAGE_ROUTE, + component: () => import('../component/app'), +}, { + path: MERGE_ROUTE, + component: () => import('../component/merge'), +}, { + path: VIRTUAL_ROUTE, + component: () => import('../component/virtual'), +}, { + path: LIMIT_ROUTE, + component: () => import('../component/limit'), +}, { + path: BACKUP_ROUTE, + component: () => import('../component/backup'), +}] + +const router = createRouter({ + history: createWebHashHistory(), + routes, +}) + +export default (app: App) => { + app.use(router) +} diff --git a/src/guide/style/dark-theme.sass b/src/guide/style/dark-theme.sass index 38b9ef1cf..d426eb9e7 100644 --- a/src/guide/style/dark-theme.sass +++ b/src/guide/style/dark-theme.sass @@ -77,4 +77,5 @@ html[data-theme='dark']:root --el-mask-color-extra-light: rgba(0 0 0 .3) --guide-container-bg-color: var(--el-fill-color-dark) // menu - --el-menu-bg-color: var(--el-fill-color-light) + --el-menu-bg-color: var(--guide-container-bg-color) + --guide-article-title-color: #DDDDDD diff --git a/src/guide/style/index.sass b/src/guide/style/index.sass index b1a9ffe1f..5acf906d8 100644 --- a/src/guide/style/index.sass +++ b/src/guide/style/index.sass @@ -7,6 +7,11 @@ @import './light-theme' @import './dark-theme' +@import '../component/common/article' + +$headerHeight: 61px +$contentHeight: calc(100vh - $headerHeight) +$asideWidth: 360px body margin: 0px @@ -15,31 +20,38 @@ body display: flex height: 100vh width: 100% - scroll-behavior: smooth + .el-header + position: relative + width: 100% + height: 60px + border-bottom: 1px var(--el-border-color) var(--el-border-style) + padding: 0 30px + .el-aside + display: block + position: absolute + left: 0 + top: $headerHeight + bottom: 0 + height: $contentHeight + width: $asideWidth + padding-top: 15px + padding-left: 15px + .el-menu + border-right: none .guide-container background: var(--guide-container-bg-color) - .el-menu - height: 100% + overflow-y: auto .el-main - height: 100% - padding: 10px 0 + position: absolute + left: $asideWidth + right: 0px + top: $headerHeight + bottom: 0 + padding: 30px 20px + // 以下待删除 .guide-area padding: 0 9vw padding-bottom: 10px - h1,h2,span,div,li - color: var(--el-text-color-primary) - h1 - font-weight: 500 - font-size: 1.8rem - letter-spacing: 0 - line-height: 1.333 - h2 - font-weight: 500 - font-size: 1.4rem - letter-spacing: 0 - line-height: 1.333 - margin: 40px 0 - margin-left: 2px .guide-paragragh,.guide-list font-size: 0.9375rem font-weight: 400 diff --git a/src/guide/style/light-theme.sass b/src/guide/style/light-theme.sass index e75ed01cf..c806d1332 100644 --- a/src/guide/style/light-theme.sass +++ b/src/guide/style/light-theme.sass @@ -10,3 +10,4 @@ --el-menu-text-color: #c1c6c8 --el-menu-hover-bg-color: #262f3e --guide-container-bg-color: var(--el-fill-color-blank) + --guide-article-title-color: #222222 diff --git a/src/guide/util.ts b/src/guide/util.ts deleted file mode 100644 index 474f93566..000000000 --- a/src/guide/util.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function position2AnchorClz(position: Position): string { - return `anchor-${position?.replace('.', '-')}` -} \ No newline at end of file diff --git a/src/i18n/chrome/message.ts b/src/i18n/chrome/message.ts index 9ef326c60..7ba5c9ea8 100644 --- a/src/i18n/chrome/message.ts +++ b/src/i18n/chrome/message.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -51,6 +51,7 @@ const placeholder: ChromeMessage = { name: '', description: '', marketName: '', + slogan: '', }, base: { currentVersion: '', diff --git a/src/i18n/message/app/index.ts b/src/i18n/message/app/index.ts index 5a7e79566..e95a1d2e2 100644 --- a/src/i18n/message/app/index.ts +++ b/src/i18n/message/app/index.ts @@ -1,11 +1,12 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import itemMessages, { ItemMessage } from "@i18n/message/common/item" +import mergeCommonMessages, { MergeCommonMessage } from "@i18n/message/common/merge" import dataManageMessages, { DataManageMessage } from "./data-manage" import reportMessages, { ReportMessage } from "./report" import analysisMessages, { AnalysisMessage } from "./analysis" @@ -27,6 +28,7 @@ import helpUsMessages, { HelpUsMessage } from "./help-us" export type AppMessage = { dataManage: DataManageMessage item: ItemMessage + mergeCommon: MergeCommonMessage report: ReportMessage whitelist: WhitelistMessage mergeRule: MergeRuleMessage @@ -49,6 +51,7 @@ const _default: Messages = { zh_CN: { dataManage: dataManageMessages.zh_CN, item: itemMessages.zh_CN, + mergeCommon: mergeCommonMessages.zh_CN, report: reportMessages.zh_CN, whitelist: whitelistMessages.zh_CN, mergeRule: mergeRuleMessages.zh_CN, @@ -69,6 +72,7 @@ const _default: Messages = { zh_TW: { dataManage: dataManageMessages.zh_TW, item: itemMessages.zh_TW, + mergeCommon: mergeCommonMessages.zh_TW, report: reportMessages.zh_TW, whitelist: whitelistMessages.zh_TW, mergeRule: mergeRuleMessages.zh_TW, @@ -89,6 +93,7 @@ const _default: Messages = { en: { dataManage: dataManageMessages.en, item: itemMessages.en, + mergeCommon: mergeCommonMessages.en, report: reportMessages.en, whitelist: whitelistMessages.en, mergeRule: mergeRuleMessages.en, @@ -109,6 +114,7 @@ const _default: Messages = { ja: { dataManage: dataManageMessages.ja, item: itemMessages.ja, + mergeCommon: mergeCommonMessages.ja, report: reportMessages.ja, whitelist: whitelistMessages.ja, mergeRule: mergeRuleMessages.ja, diff --git a/src/i18n/message/app/menu.ts b/src/i18n/message/app/menu.ts index aee5d5e74..73d1aa7ce 100644 --- a/src/i18n/message/app/menu.ts +++ b/src/i18n/message/app/menu.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -65,7 +65,7 @@ const _default: Messages = { feedback: '有什麼反饋嗎?', rate: '打個分吧!', helpUs: '帮助我们~', - userManual: '用戶手冊', + userManual: '使用者手冊', }, en: { dashboard: 'Dashboard', diff --git a/src/i18n/message/app/merge-rule.ts b/src/i18n/message/app/merge-rule.ts index 87a91092f..544f0caa5 100644 --- a/src/i18n/message/app/merge-rule.ts +++ b/src/i18n/message/app/merge-rule.ts @@ -1,13 +1,11 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ export type MergeRuleMessage = { - resultOfOrigin: string - resultOfLevel: string removeConfirmMsg: string originPlaceholder: string mergedPlaceholder: string @@ -25,8 +23,6 @@ export type MergeRuleMessage = { const _default: Messages = { zh_CN: { - resultOfOrigin: '不合并', - resultOfLevel: '{level} 级域名', removeConfirmMsg: '自定义合并规则 {origin} 将被移除', originPlaceholder: '原域名', mergedPlaceholder: '合并后域名', @@ -42,8 +38,6 @@ const _default: Messages = { infoAlert5: '如果没有命中任何规则,则默认会合并至 {psl} 的前一级', }, zh_TW: { - resultOfOrigin: '不合並', - resultOfLevel: '{level} 級網域', removeConfirmMsg: '自定義合並規則 {origin} 將被移除', originPlaceholder: '原網域', mergedPlaceholder: '合並後網域', @@ -59,8 +53,6 @@ const _default: Messages = { infoAlert5: '如果沒有匹配任何規則,則默認會合並至 {psl} 的前一級', }, en: { - resultOfOrigin: 'Not Merge', - resultOfLevel: 'Keep Level {level}', removeConfirmMsg: '{origin} will be removed from customized merge rules.', originPlaceholder: 'Origin site', mergedPlaceholder: 'Merged', @@ -76,8 +68,6 @@ const _default: Messages = { infoAlert5: 'If no rule is matched, it will default to the level before {psl}', }, ja: { - resultOfOrigin: '不合并', - resultOfLevel: '{level} 次ドメイン', removeConfirmMsg: 'カスタム マージ ルール {origin} は削除されます', originPlaceholder: '独自ドメイン名', mergedPlaceholder: '統計的ドメイン名', diff --git a/src/i18n/message/app/option.ts b/src/i18n/message/app/option.ts index dd34d377e..2c1ab322d 100644 --- a/src/i18n/message/app/option.ts +++ b/src/i18n/message/app/option.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -84,13 +84,6 @@ export type OptionMessage = { defaultValue: string } -const FOLLOW_BROWSER: Record = { - zh_CN: '跟随浏览器', - zh_TW: '跟隨瀏覽器', - en: 'Follow browser', - ja: 'ブラウザと同じ', -} - const _default: Messages = { zh_CN: { yes: '是', @@ -161,12 +154,12 @@ const _default: Messages = { auth: '', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: '需要创建一个至少包含 gist 权限的 token', }, }, - alert: '这是一项实验性功能,如果有任何问题请联系作者~ (returnzhy1996@outlook.com)', + alert: '这是一项实验性功能,如果有任何问题请联系作者~ ({email})', test: '测试', lastTimeTip: '上次备份时间: {lastTime}', operation: '备份数据', @@ -247,12 +240,12 @@ const _default: Messages = { label: '關閉備份', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: '需要創建一個至少包含 gist 權限的 token', }, }, - alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 (returnzhy1996@outlook.com) ~', + alert: '這是一項實驗性功能,如果有任何問題請聯繫作者 ({email}) ~', test: '測試', operation: '備份數據', lastTimeTip: '上次備份時間: {lastTime}', @@ -333,12 +326,12 @@ const _default: Messages = { label: 'Always off', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: 'One token with at least gist permission is required', }, }, - alert: 'This is an experimental feature, if you have any questions please contact the author via returnzhy1996@outlook.com~', + alert: 'This is an experimental feature, if you have any questions please contact the author via {email}~', test: 'Test', operation: 'Backup', lastTimeTip: 'Last backup time: {lastTime}', @@ -419,12 +412,12 @@ const _default: Messages = { label: 'バックアップを有効にしない', }, gist: { - label: 'Github Gist', + label: 'GitHub Gist', auth: 'Personal Access Token {info} {input}', authInfo: '少なくとも gist 権限を持つトークンが 1 つ必要です', }, }, - alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください (returnzhy1996@outlook.com)', + alert: 'これは実験的な機能です。質問がある場合は、作成者に連絡してください ({email})', test: 'テスト', operation: 'バックアップ', lastTimeTip: '前回のバックアップ時間: {lastTime}', diff --git a/src/i18n/message/common/base.ts b/src/i18n/message/common/base.ts index 042b44eeb..56d9331fe 100644 --- a/src/i18n/message/common/base.ts +++ b/src/i18n/message/common/base.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -28,7 +28,7 @@ const _default: Messages = { zh_TW: { currentVersion: '版本: v{version}', allFunction: '所有功能', - guidePage: '用戶手冊', + guidePage: '使用者手冊', }, ja: { currentVersion: 'バージョン: v{version}', diff --git a/src/i18n/message/common/merge.ts b/src/i18n/message/common/merge.ts new file mode 100644 index 000000000..3f056d4b4 --- /dev/null +++ b/src/i18n/message/common/merge.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type MergeCommonMessage = { + tagResult: { + blank: string + level: string + } +} + +const messages: Messages = { + en: { + tagResult: { + blank: 'Not Merge', + level: 'Keep Level {level}', + } + }, + zh_CN: { + tagResult: { + blank: '不合并', + level: '{level} 级域名', + }, + }, + zh_TW: { + tagResult: { + blank: '不合並', + level: '{level} 級網域', + } + }, + ja: { + tagResult: { + blank: '不合并', + level: '{level} 次ドメイン', + } + } +} + +export default messages \ No newline at end of file diff --git a/src/i18n/message/common/meta.ts b/src/i18n/message/common/meta.ts index 43b9b2b0e..c26eb7266 100644 --- a/src/i18n/message/common/meta.ts +++ b/src/i18n/message/common/meta.ts @@ -1,15 +1,25 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ export type MetaMessage = { name: string marketName: string description: string + slogan: string } +const SLOGAN_EN = 'Insight & Improve' + const _default: Messages = { zh_CN: { name: '网费很贵', marketName: '网费很贵 - 上网时间统计', description: '做最好用的上网时间统计工具。', + slogan: SLOGAN_EN, }, zh_TW: { name: '網費很貴', @@ -25,6 +35,7 @@ const _default: Messages = { name: 'Timer', marketName: 'Timer - Browsing Time & Visit count', description: 'To be the BEST web timer.', + slogan: SLOGAN_EN, }, } diff --git a/src/i18n/message/guide/app.ts b/src/i18n/message/guide/app.ts new file mode 100644 index 000000000..a977f7054 --- /dev/null +++ b/src/i18n/message/guide/app.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type AppMessage = { + title: string + p1: string + l1: string + l2: string + p2: string +} + +const _default: Messages = { + en: { + title: 'Enter the management page', + p1: 'Based on icons, the extension provides a more convenient way to view data. ' + + 'But if you want to experience full functionality, ' + + 'you need to visit the management page of the extension, via one of the following two ways.', + l1: 'You can right-click the icon of the extension, and click [{button}] in the pop-up menu.', + l2: 'You can also find the [{button}] link at the bottom of the icon popup page, just click it.', + p2: 'The popup page and management page are the main interaction methods of this extension. After you know how to open them, you can use it completely.', + }, + zh_CN: { + title: '进入后台管理页面', + p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的后台管理页。进入后台页有以下两种方式。', + l1: '您可以右击扩展的图标,在弹出的菜单中点击【{button}】。', + l2: '您也可以在图标弹出页的页脚找到【{button}】链接,同样地,点击它即可。', + p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。' + }, + zh_TW: { + title: '進入管理頁面', + p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的後台管理頁。進入後台頁有以下兩種方式。', + l1: '您可以右擊擴展的圖標,在彈出的菜單中點擊【{button}】。', + l2: '您也可以在圖標彈出頁的頁腳找到【{button}】鏈接,同樣地,點擊它即可。', + p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', + }, + ja: { + title: '管理ページに入る', + p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、その完全な機能を体験したい場合は、拡張バックグラウンド管理ページにアクセスする必要があります. バックグラウンド ページに入る方法は 2 つあります。', + l1: '拡張機能のアイコンを右クリックして、ポップアップ メニューの [{button}] をクリックします。', + l2: 'アイコン ポップアップ ページのフッターに [{button}] リンクがあり、同じ方法でクリックすることもできます。', + p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', + }, +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/backup.ts b/src/i18n/message/guide/backup.ts new file mode 100644 index 000000000..f21e5b49f --- /dev/null +++ b/src/i18n/message/guide/backup.ts @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type BackupMessage = { + title: String + p1: string + upload: { + title: string + prepareToken: string + enter: string + form: string + backup: string + } + query: { + title: string + p1: string + enter: string + enable: string + wait: string + tip: string + } +} + +const _default: Messages = { + en: { + title: 'Backup data with GitHub Gist', + p1: 'This extension supports users to backup data using GitHub Gist{link} through simple settings.', + upload: { + title: 'Four simple steps to complete the setup', + prepareToken: 'First, generate a token with gist permissions on GitHub{link}.', + enter: 'Enter the option page{link}.', + form: 'Then select GitHub Gist as the synchronization method, ' + + 'and fill in your token in the input box that appears below.', + backup: 'Click the Backup button to upload local data to your GitHub Gist.', + }, + query: { + title: 'How to query data backed up by other browsers?', + p1: 'If you correctly set the token in the above steps, you can query remote data in just three simple steps.', + enter: 'First, enter the management page{link}, click the menu item {menuItem}.', + enable: 'If the token is set correctly, an icon, like {icon}, will appear in the upper right corner of the page, ' + + 'click it to enable the remote query.', + wait: 'Wait for the data query to complete, and move the mouse over the value to view the data of each client.', + tip: 'Because remote data is stored in monthly shards, the query time period should not be too long.', + } + }, + zh_CN: { + title: '使用 GitHub Gist 备份数据', + p1: '这个扩展支持用户通过简单的设置,使用 GitHub Gist{link} 备份数据。', + upload: { + title: '简单四步完成设置', + prepareToken: '首先,您需要在 GitHub 生成一个包含 gist 权限的 token{link}。', + enter: '进入扩展的选项页面{link}。', + form: '然后将同步方式选为 GitHub Gist,将你的 token 填入下方出现的输入框中。', + backup: '最后,点击备份按钮即可将本地数据导入到你的 gist 里。' + }, + query: { + title: '如何查询其他浏览器备份的数据?', + p1: '如果您在上述步骤中正确设置了 token,只需简单三步即可查询远端数据。', + enter: '首先,进入管理页{link},点击菜单项【{menuItem}】。', + enable: '如果 token 设置正确,页面右上角会出现一个{icon}图标,点击它即可开启远端查询。', + wait: '等待数据查询完毕,将鼠标移动到数值上,即可查看每个客户端的数据。', + tip: '因为远端数据时按月份分片存放,所以查询时间段不宜过长。', + } + }, + zh_TW: { + title: '使用 GitHub Gist 備份數據', + p1: '這個擴展支持用戶通過簡單的設置,使用 GitHub Gist{link} 備份數據。', + upload: { + title: '簡單四步完成設置', + prepareToken: '首先,您需要在 GitHub 生成一個包含 gist 權限的 token{link}。', + enter: '進入擴展的選項頁面{link}。', + form: '然後將同步方式選為 GitHub Gist,將你的 token 填入下方出現的輸入框中。', + backup: '最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。' + }, + query: { + title: '如何查詢其他瀏覽器備份的數據?', + p1: '如果您在上述步驟中正確設置了 token,只需簡單三步即可查詢遠端數據。', + enter: '首先,進入管理頁{link},點擊菜單項【{menuItem}】。', + enable: '如果 token 設置正確,頁面右上角會出現一個{icon}圖標,點擊它即可開啟遠端查詢。', + wait: '等待數據查詢完畢,將鼠標移動到數值上,即可查看每個客戶端的數據。', + tip: '因為遠端數據時按月份分片存放,所以查詢時間段不宜過長。', + } + }, + ja: { + title: 'GitHub Gist でデータをバックアップする', + p1: 'この拡張機能は、簡単な設定で GitHub Gist{link} を使用してデータをバックアップするユーザーをサポートします。', + upload: { + title: 'セットアップを完了するための 4 つの簡単なステップ', + prepareToken: 'まず、GitHub{link} で Gist 権限を持つトークンを生成します。', + enter: 'オプションページ{link}に入ります。', + form: '次に、同期方法として GitHub Gist を選択し、下に表示される入力ボックスにトークンを入力します。', + backup: '[バックアップ] ボタンをクリックして、ローカル データを GitHub Gist にアップロードします。', + }, + query: { + title: '他のブラウザでバックアップされたデータを照会する方法は?', + p1: '上記の手順でトークンを正しく設定すると、わずか 3 つの簡単な手順でリモート データをクエリできます。', + enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', + enable: 'トークンが正しく設定されている場合、{icon} のようなアイコンがページの右上隅に表示されるので、それをクリックしてリモート クエリを有効にします。', + wait: 'データ クエリが完了するのを待ち、値の上にマウスを移動して、各クライアントのデータを表示します。', + tip: 'リモート データは毎月のシャードに保存されるため、クエリ期間が長すぎないようにする必要があります。', + } + }, +} + +export default _default diff --git a/src/i18n/message/guide/home.ts b/src/i18n/message/guide/home.ts new file mode 100644 index 000000000..58197aff5 --- /dev/null +++ b/src/i18n/message/guide/home.ts @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +type _Key = + | 'desc' + | 'button' + | 'download' + +export type HomeMessage = { + [key in _Key]: string +} + +const _default: Messages = { + en: { + desc: 'Timer can help you track the time you spent on browsing websites and the count of visit, with what you can insight and improve your web habits.', + button: 'Start Now!', + download: 'Install for {browser}', + }, + zh_CN: { + desc: '网费很贵是一个开源、免费的上网时间统计插件。它可以帮助您统计每天在每个网站上所花费的时间和访问次数。您可以借此来观察您的上网习惯,并通过为指定网站设置每天的浏览上限来改善它。', + button: '如何使用', + download: '在 {browser} 上安装', + }, + zh_TW: { + desc: '網費很貴是一個開源、免費的上網時間統計插件。它可以幫助您統計每天在每個網站上所花費的時間和訪問次數。您可以藉此來觀察您的上網習慣,並通過為指定網站設置每天的瀏覽上限來改善它。', + button: '如何使用', + download: '在 {browser} 上安裝', + }, + ja: { + desc: 'この拡張機能は、ウェブサイトの閲覧に費やした時間と訪問回数を追跡するのに役立ち、ウェブ習慣を洞察して改善することができます.', + button: 'すぐに始めましょう', + download: '{browser} にインストール', + } +} + +export default _default diff --git a/src/i18n/message/guide/index.ts b/src/i18n/message/guide/index.ts index 399d156aa..d11cb7325 100644 --- a/src/i18n/message/guide/index.ts +++ b/src/i18n/message/guide/index.ts @@ -1,58 +1,100 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ import layoutMessages, { LayoutMessage } from "./layout" -import profileMessages, { ProfileMessage } from "./profile" -import usageMessages, { UsageMessage } from "./usage" +import startMessages, { StartMessage } from "./start" import privacyMessages, { PrivacyMessage } from "./privacy" import metaMessages, { MetaMessage } from "../common/meta" import baseMessages, { BaseMessage } from "../common/base" +import homeMessages, { HomeMessage } from "./home" +import appMessages, { AppMessage } from "./app" +import mergeMessages, { MergeMessage } from "./merge" +import virtualMessages, { VirtualMessage } from "./virtual" +import limitMessages, { LimitMessage } from "./limit" +import backupMessages, { BackupMessage } from "./backup" +import mergeCommonMessages, { MergeCommonMessage } from "../common/merge" +import appMenuMessages, { MenuMessage as AppMenuMessage } from "../app/menu" export type GuideMessage = { + mergeCommon: MergeCommonMessage layout: LayoutMessage - profile: ProfileMessage - usage: UsageMessage + home: HomeMessage + start: StartMessage privacy: PrivacyMessage meta: MetaMessage base: BaseMessage + app: AppMessage + merge: MergeMessage + virtual: VirtualMessage + limit: LimitMessage + backup: BackupMessage + appMenu: AppMenuMessage } const _default: Messages = { zh_CN: { + mergeCommon: mergeCommonMessages.zh_CN, layout: layoutMessages.zh_CN, - profile: profileMessages.zh_CN, - usage: usageMessages.zh_CN, + home: homeMessages.zh_CN, + start: startMessages.zh_CN, privacy: privacyMessages.zh_CN, meta: metaMessages.zh_CN, base: baseMessages.zh_CN, + app: appMessages.zh_CN, + merge: mergeMessages.zh_CN, + virtual: virtualMessages.zh_CN, + limit: limitMessages.zh_CN, + backup: backupMessages.zh_CN, + appMenu: appMenuMessages.zh_CN, }, zh_TW: { + mergeCommon: mergeCommonMessages.zh_TW, layout: layoutMessages.zh_TW, - profile: profileMessages.zh_TW, - usage: usageMessages.zh_TW, + home: homeMessages.zh_TW, + start: startMessages.zh_TW, privacy: privacyMessages.zh_TW, meta: metaMessages.zh_TW, base: baseMessages.zh_TW, + app: appMessages.zh_TW, + merge: mergeMessages.zh_TW, + virtual: virtualMessages.zh_TW, + limit: limitMessages.zh_TW, + backup: backupMessages.zh_TW, + appMenu: appMenuMessages.zh_TW, }, en: { + mergeCommon: mergeCommonMessages.en, layout: layoutMessages.en, - profile: profileMessages.en, - usage: usageMessages.en, + home: homeMessages.en, + start: startMessages.en, privacy: privacyMessages.en, meta: metaMessages.en, base: baseMessages.en, + app: appMessages.en, + merge: mergeMessages.en, + virtual: virtualMessages.en, + limit: limitMessages.en, + backup: backupMessages.en, + appMenu: appMenuMessages.en, }, ja: { + mergeCommon: mergeCommonMessages.ja, layout: layoutMessages.ja, - profile: profileMessages.ja, - usage: usageMessages.ja, + home: homeMessages.ja, + start: startMessages.ja, privacy: privacyMessages.ja, meta: metaMessages.ja, base: baseMessages.ja, + app: appMessages.ja, + merge: mergeMessages.ja, + virtual: virtualMessages.ja, + limit: limitMessages.ja, + backup: backupMessages.ja, + appMenu: appMenuMessages.ja, }, } diff --git a/src/i18n/message/guide/layout.ts b/src/i18n/message/guide/layout.ts index 4be2cc2ac..26cd489f1 100644 --- a/src/i18n/message/guide/layout.ts +++ b/src/i18n/message/guide/layout.ts @@ -1,95 +1,55 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ export type LayoutMessage = { + header: { + sourceCode: string + email: string + } menu: { - profile: string - usage: { - title: string - quickstart: string - background: string - advanced: string - backup: string - } - privacy: { - title: string - scope: string - storage: string - } + usage: string } } const _default: Messages = { zh_CN: { + header: { + sourceCode: '查看源代码', + email: '联系作者', + }, menu: { - profile: '欢迎安装{appName}', - usage: { - title: '如何使用', - quickstart: '快速开始', - background: '访问后台页面', - advanced: '高级功能', - backup: '使用 Gist 备份数据', - }, - privacy: { - title: '隐私声明', - scope: '收集哪些数据', - storage: '如何处理这些数据', - }, + usage: '高级用法' }, }, zh_TW: { + header: { + sourceCode: '查看源代碼', + email: '聯繫作者', + }, menu: { - profile: '歡迎安裝{appName}', - usage: { - title: '如何使用', - quickstart: '快速開始', - background: '訪問後台頁面', - advanced: '高級功能', - backup: '使用 Gist 備份數據', - }, - privacy: { - title: '隱私聲明', - scope: '收集哪些數據', - storage: '如何處理這些數據', - }, + usage: '高級用法', }, }, en: { + header: { + sourceCode: 'View source code', + email: 'Contact author' + }, menu: { - profile: 'Welcome to install {appName}', - usage: { - title: 'Using Timer', - quickstart: 'Quickstart', - background: 'Using all functions', - advanced: 'Advanced features', - backup: 'Backup your data with Gist', - }, - privacy: { - title: 'Privary Policy', - scope: 'Personal data collected', - storage: 'How to do with this data', - }, + usage: 'Advanced usages', }, }, ja: { + header: { + sourceCode: 'ソースコードを見る', + email: '著者に連絡する', + }, menu: { - profile: '{appName}へようこそ', - usage: { - title: '使い方', - quickstart: 'クイックスタート', - background: 'すべての機能', - advanced: '高度な機能', - backup: 'Gist でデータをバックアップ', - }, - privacy: { - title: 'ポリシーと規約', - scope: '収集する情報', - storage: 'このデータをどうするか', - }, + usage: '高度な使い方', }, }, } diff --git a/src/i18n/message/guide/limit.ts b/src/i18n/message/guide/limit.ts new file mode 100644 index 000000000..a9bf84895 --- /dev/null +++ b/src/i18n/message/guide/limit.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type LimitMessage = { + title: string + p1: string + step: { + title: string + enter: string + click: string + form: string + check: string + } +} + +const _default: Messages = { + en: { + title: 'Limit browsing time of everyday', + p1: 'If you want to limit the time of browsing certain URLs each day, you can do so by creating a daily time limit rule.', + step: { + title: 'Four steps to create a limit rule', + enter: 'First, enter the management page{link}, click the menu item {menuItem}.', + click: 'Click the New button in the upper right corner.', + form: 'Paste the URL that needs to be restricted, and the duration of the restriction. ' + + 'Some URL fragments can be replaced with wildcards as needed, or deleted directly. Then click Save.', + check: 'Finally, check whether the target URL hits the newly added rule through the Test button in the upper right corner.', + } + }, + zh_CN: { + title: '限制每天的浏览时间', + p1: '如果你想限制每天浏览某些 URL 的时长,可以通过创建每日时限规则来完成。', + step: { + title: '简单四步创建一个限制规则', + enter: '首先进入管理页{link},点击菜单项【{menuItem}】。', + click: '然后单击右上角的新建按钮。', + form: '粘贴需要限制的 URL,以及限制时长。可以根据需要将部分 URL 片段使用通配符代替,或者直接删除。然后点击保存。', + check: '最后通过右上角的测试功能检查目标 URL 是否命中了刚添加的规则。', + } + }, + zh_TW: { + title: '限制每天的瀏覽時間', + p1: '如果你想限制每天瀏覽某些 URL 的時長,可以通過創建每日時限規則來完成。', + step: { + title: '簡單四步創建一個限制規則', + enter: '首先進入管理頁{link},點擊菜單項【{menuItem}】。', + click: '然後單擊右上角的新建按鈕。', + form: '粘貼需要限制的 URL,以及限制時長。可以根據需要將部分 URL 片段使用通配符代替,或者直接刪除。然後點擊保存。', + check: '最後通過右上角的測試功能檢查目標 URL 是否命中了剛添加的規則。', + } + }, + ja: { + title: '毎日の閲覧時間を制限する', + p1: '特定の URL を毎日閲覧する時間を制限したい場合は、毎日の時間制限ルールを作成することでこれを行うことができます。', + step: { + title: '制限ルールを作成するための 4 つのステップ', + enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', + click: '右上隅にある [新規] ボタンをクリックします。', + form: '制限する必要がある URL と制限の期間を貼り付けます。' + + '一部の URL フラグメントは、必要に応じてワイルドカードに置き換えたり、直接削除したりできます。 次に、[保存] をクリックします。', + check: '最後に、右上隅の [テスト] ボタンを使用して、ターゲット URL が新しく追加されたルールに一致するかどうかを確認します。', + } + }, +} + +export default _default diff --git a/src/i18n/message/guide/merge.ts b/src/i18n/message/guide/merge.ts new file mode 100644 index 000000000..a6168ee12 --- /dev/null +++ b/src/i18n/message/guide/merge.ts @@ -0,0 +1,184 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type MergeMessage = { + title: string + p1: string + p2: string + lookTitle: string + p3: string + sourceCol: string + targetCol: string + remarkCol: string + source: { + title: string + p1: string + exampleCol: string + only: string + } + target: { + title: string + p1: string + lookCol: string + remark: { + blank: string + spec: string + integer: string + specFirst: string + miss: string + } + p2: string + originalCol: string + mergedCol: string + hitCol: string + } +} + +const _default: Messages = { + en: { + title: 'Summarize data of related sites', + p1: 'This extension is counted by domain name, for example, {demo1} and {demo2} will be counted as 2 records. ' + + 'If you want to see aggregated data for both sites, you\'ll need to use the merge feature.', + p2: 'On most data display pages, merged queries are supported. And users can customize the merge rules on the background page{link}.', + lookTitle: 'What\'s rules look like?', + p3: 'The rule consists of two parts, the source part and the target part. ' + + 'The source part declares which sites hit the rule, and the target part defines how those sites are merged. ' + + 'For example, {demo} is a common rule, the left is the source site, and the right is the target site.', + sourceCol: 'Source part', + targetCol: 'Target part', + remarkCol: 'Remark', + source: { + title: 'How to define the source part?', + p1: 'The source part can be a specific domain name or an Ant expression. Below are some examples.', + exampleCol: 'Examples of matched site', + only: "Only {source} can hit this rule" + }, + target: { + title: 'How to define the target part?', + p1: 'The target part can be a specific domain name, a positive integer, or be left blank. ' + + 'They will be introduced one by one in conjunction with the source part in the table below.', + + lookCol: 'Look', + remark: { + blank: '{source} won\'t be merged cause of blank target part', + spec: 'Sites hitting {source} will be merged into the specific {target}', + integer: 'Sites hitting {source} will be merged into the last {target} level domain names', + specFirst: 'When multiple rules are hit, the source part takes precedence for a specific domain name', + miss: 'Merge to the before level of Public Suffix List{link} when no rules are hit', + }, + p2: 'The following table is some merging examples after the above rules are set at the same time.', + originalCol: 'Original site', + mergedCol: 'Merged site', + hitCol: 'Hitted rule', + } + }, + zh_CN: { + title: '合并统计相关站点', + p1: '这个扩展是按照域名进行统计的,比方说 {demo1} 和 {demo2} 会被统计到 2 个条目。' + + '如果您想要查看这两个网站的数据汇总的话,就需要使用合并功能。', + p2: '在大多数的数据展示界面,都支持站点合并查询。并且用户可以在后台页自定义合并规则{link}。', + lookTitle: '规则长什么样?', + p3: '规则由两部分组成,源和目标。源部分声明哪些站点会命中该规则,而目标部分定义如何合并这些站点。' + + '比如 {demo} 就是一个常见的规则,左边的是源,右边的是目标。', + source: { + title: '如何定义源部分?', + p1: '源部分可以是具体的域名,也可以是 Ant 表达式。下面是一些例子。', + exampleCol: '匹配网站的示例', + only: '只有 {source} 能够命中该规则', + }, + sourceCol: '源部分', + targetCol: '目标部分', + remarkCol: '备注', + target: { + title: '如何定义目标部分?', + p1: '目标部分可以是具体的域名,正整数,或者留空。将在下表中结合源部分一一介绍。', + lookCol: '规则外观', + remark: { + blank: '{source} 不会被合并,因为目标部分为空', + spec: '满足 {source} 的网站会被合并到指定条目 {target}', + integer: '满足 {source} 的网站,在合并时会保留后 {target} 级域名', + specFirst: '命中多个规则时,源部分是具体域名的优先', + miss: '没有命中任何规则时,合并至 Public Suffix List{link} 的前一级', + }, + p2: '下表是上述规则同时设置后的一些合并示例。', + originalCol: '原始站点', + mergedCol: '合并后站点', + hitCol: '命中的规则', + }, + }, + zh_TW: { + title: '合併統計相關站點', + p1: '這個擴展是按照域名進行統計的,比方說 {demo1} 和 {demo2} 會被統計到 2 個條目。如果您想要查看這兩個網站的數據匯總的話,就需要使用合併功能。', + p2: '在大多數的數據展示界面,都支持站點合併查詢。並且用戶可以在後台頁自定義合併規則{link}。', + lookTitle: '規則長什麼樣?', + p3: '規則由兩部分組成,源和目標。源部分聲明哪些站點會命中該規則,而目標部分定義如何合併這些站點。比如 {demo} 就是一個常見的規則,左邊的是源,右邊的是目標。', + source: { + title: '如何定義源部分?', + p1: '源部分可以是具體的域名,也可以是 Ant 表達式。下面是一些例子。', + exampleCol: '匹配網站的示例', + only: '只有 {source} 能够命中该规则', + }, + sourceCol: '源部分', + targetCol: '目標部分', + remarkCol: '備註', + target: { + title: '如何定義目標部分?', + p1: '目標部分可以是具體的域名,正整數,或者留空。將在下表中結合源部分一一介紹。', + lookCol: '規則外觀', + remark: { + blank: '{source} 不会被合并,因为目标部分为空', + spec: '滿足 {source} 的網站會被合併到指定條目 {target}', + integer: '滿足 {source} 的網站,在合併時會保留後 {target} 級域名', + specFirst: '命中多個規則時,源部分是具體域名的優先', + miss: '沒有命中任何規則時,合併至 Public Suffix List{link} 的前一級', + }, + p2: '下表是上述規則同時設置後的一些合併示例。', + originalCol: '原始站點', + mergedCol: '合併後站點', + hitCol: '命中的規則', + }, + }, + ja: { + title: '関連サイトのデータをまとめます', + p1: 'この拡張子はドメイン名でカウントされます。たとえば、{demo1} と {demo2} は 2 つのレコードとしてカウントされます。' + + '両方のサイトの集計データを表示する場合は、マージ機能を使用する必要があります。', + p2: 'ほとんどのデータ表示ページでは、マージされたクエリがサポートされています。 また、ユーザーはバックグラウンド ページ{link}でマージ ルールをカスタマイズできます。', + lookTitle: 'ルールはどのように見えますか?', + p3: 'ルールは、ソース部分とターゲット部分の 2 つの部分で構成されます。' + + 'ソース部分はルールに一致するサイトを宣言し、ターゲット部分はそれらのサイトがどのようにマージされるかを定義します。' + + 'たとえば、{demo} は一般的なルールで、左側がソース サイト、右側がターゲット サイトです。', + sourceCol: 'ソース部分', + targetCol: '対象部位', + remarkCol: '述べる', + source: { + title: 'ソース パーツを定義する方法', + p1: 'ソース部分は、特定のドメイン名または Ant 式にすることができます。 以下にいくつかの例を示します。', + exampleCol: 'マッチしたサイトの例', + only: "このルールに該当するのは {source} だけです" + }, + target: { + title: 'ターゲット パーツを定義する方法', + p1: 'ターゲット部分は、特定のドメイン名、正の整数、または空白のままにすることができます。' + + '下表のソース部分と合わせて順次紹介していきます。', + + lookCol: '外観', + remark: { + blank: '{source} はマージされません 空白のターゲット パーツが原因です', + spec: '{source} にヒットしたサイトは、特定の {target} にマージされます', + integer: '{source} にヒットしたサイトは、最後の {target} レベルのドメイン名にマージされます', + specFirst: '複数のルールがヒットした場合、特定のドメイン名についてソース部分が優先されます', + miss: 'ルールにヒットしない場合は、Public Suffix List{link} の前のレベルにマージします', + }, + p2: '次の表は、上記のルールを同時に設定した後のいくつかのマージ例です。', + originalCol: '元のサイト', + mergedCol: '統合サイト', + hitCol: 'ヒットルール', + } + }, +} + +export default _default diff --git a/src/i18n/message/guide/privacy.ts b/src/i18n/message/guide/privacy.ts index 7e8401c28..be36a95d7 100644 --- a/src/i18n/message/guide/privacy.ts +++ b/src/i18n/message/guide/privacy.ts @@ -1,75 +1,181 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ - type _StoreKey = + | 'title' | 'p1' | 'p2' | 'p3' -type _ScopeKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' + +type RequiredScope = { + name: string + usage: string +} + +type OptionalScope = RequiredScope & { + optionalReason: string +} + +export type Scope = RequiredScope & { + optionalReason?: string +} export type PrivacyMessage = { - scope: { [key in _ScopeKey]: string } + title: string + alert: string + scope: { + title: string + cols: { + name: string + usage: string + required: string + } + rows: { + website: RequiredScope + tab: OptionalScope + clipboard: OptionalScope + } + } storage: { [key in _StoreKey]: string } } const _default: Messages = { zh_CN: { + title: '隐私声明', + alert: '为了向您提供完整的服务,该扩展在使用过程中会必要地收集您的一些个人数据,详情见以下隐私声明。', scope: { - p1: '为了向您提供完整的服务,该扩展在使用过程中会收集您以下的个人信息:', - l1: '1. 您浏览网站的时间,以及访问每个网站的次数。', - l2: '2. 网站的标题以及图标 URL。', - l3: '3. 为了提高用户体验,扩展在必要时会征得您的授权之后,读取您的剪切板内容。', + title: '哪些数据会被收集?', + cols: { + name: '内容', + usage: '用途', + required: '是否必需', + }, + rows: { + website: { + name: '网站访问记录', + usage: '用于统计浏览时长和访问次数', + }, + tab: { + name: '浏览器标签信息', + usage: '用于自动获取网站的名称和图标,展示数据时提升用户体验', + optionalReason: '只有在选项里开启自动获取功能后才会收集', + }, + clipboard: { + name: '剪切板内容', + usage: '在设置每日时限规则时,为了操作方便,会读取剪切板内 URL', + optionalReason: '需要用户手动同意' + }, + }, }, storage: { + title: '如何处理这些数据?', p1: '我们保证该扩展收集的所有数据只会保存在您的浏览器本地存储中,绝不会将他们分发到其他地方。', p2: '不过您可以使用扩展提供的工具,以 JSON 或者 CSV 的文件格式,导出或者导入您的数据。扩展也支持您使用 GitHub Gist 等,您足以信任的第三方服务,备份您的数据。', p3: '我们只帮助您收集数据,但处置权一定在您。', }, }, zh_TW: { + title: '隱私聲明', + alert: '為了向您提供完整的服務,該擴展在使用過程中會必要地收集您的一些個人數據,詳情見以下隱私聲明。', scope: { - p1: '為了向您提供完整的服務,該擴展在使用過程中會收集您以下的個人信息:', - l1: '1. 您瀏覽網站的時間,以及訪問每個網站的次數。', - l2: '2. 網站的標題以及圖標 URL。', - l3: '3. 為了提高用戶體驗,擴展在必要時會徵得您的授權之後,讀取您的剪切板內容。', + title: '哪些數據會被收集?', + cols: { + name: '內容', + usage: '用途', + required: '是否必需', + }, + rows: { + website: { + name: '網站訪問記錄', + usage: '用於統計瀏覽時長和訪問次數', + }, + tab: { + name: '瀏覽器標籤信息', + usage: '用於自動獲取網站的名稱和圖標,展示數據時提升用戶體驗', + optionalReason: '只有在選項裡開啟自動獲取功能後才會收集', + }, + clipboard: { + name: '剪切板內容', + usage: '在設置每日時限規則時,為了操作方便,會讀取剪切板內 URL', + optionalReason: '需要用戶手動同意' + }, + }, }, storage: { + title: '如何處理這些數據?', p1: '我們保證該擴展收集的所有數據只會保存在您的瀏覽器本地存儲中,絕不會將他們分發到其他地方。', p2: '不過您可以使用擴展提供的工具,以 JSON 或者 CSV 的文件格式,導出或者導入您的數據。擴展也支持您使用 GitHub Gist 等,您足以信任的第三方服務,備份您的數據。', p3: '我們只幫助您收集數據,但處置權一定在您。', }, }, en: { + title: 'Privacy statement', + alert: 'In order to provide you with complete services, this extension will necessarily collect some of your personal data during use, see the following privacy statement for details.', scope: { - p1: 'In order to provide you with complete services, this extension will collect your following personal information during use:', - l1: '1. How long you browse the site, and how many times you visit each site.', - l2: '2. The title of the website and the URL of the icon.', - l3: '3. In order to improve user experience, the extension will read your clipboard content after obtaining your authorization when necessary.', + title: 'What data is collected?', + cols: { + name: 'Content', + usage: 'Usage', + required: 'Required', + }, + rows: { + website: { + name: 'Website browsing history', + usage: 'Used to count browsing time and visits', + }, + tab: { + name: 'Tab information', + usage: 'Used to automatically obtain the name and icon of the website, and improve user experience when displaying data', + optionalReason: 'Only if this function is enabled in the options', + }, + clipboard: { + name: 'Clipboard content', + usage: 'When setting the daily time limit rule, for the convenience of operation, the URL in the clipboard will be read', + optionalReason: 'Only if user agreed' + }, + }, }, storage: { + title: 'How to do with this data?', p1: 'We guarantee that all data collected by this extension will only be saved in your browser\'s local storage and will never be distributed elsewhere.', p2: 'You can however use the tools provided by the extension to export or import your data in JSON or CSV file format. The extension also supports you to use GitHub Gist, etc., third-party services you trust enough to back up your data.', p3: 'We only help you collect data, but the right of disposal must be yours.', }, }, ja: { + title: 'プライバシーに関する声明', + alert: '完全なサービスを提供するために、この拡張機能は使用中に必ず個人データの一部を収集します。詳細については、次のプライバシーに関する声明を参照してください。', scope: { - p1: '完全なサービスを提供するために、この拡張機能は使用中に次の個人情報を収集します。', - l1: '1. サイトを閲覧した時間と、各サイトにアクセスした回数。', - l2: '2. ウェブサイトのタイトルとアイコンの URL。', - l3: '3. ユーザー エクスペリエンスを向上させるために、拡張機能は必要に応じて承認を得た後、クリップボードの内容を読み取ります。', + title: 'どのようなデータが収集されますか?', + cols: { + name: '収集データ', + usage: '使用', + required: 'それは必要ですか', + }, + rows: { + website: { + name: 'ウェブサイトの閲覧履歴', + usage: '閲覧時間と訪問をカウントするために使用されます', + }, + tab: { + name: 'タブ情報', + usage: 'Web サイトの名前とアイコンを自動的に取得し、データを表示する際のユーザー エクスペリエンスを向上させるために使用されます', + optionalReason: 'この機能がオプションで有効になっている場合のみ', + }, + clipboard: { + name: 'クリップボードの内容', + usage: '毎日の時間制限ルールを設定すると、操作の便宜上、クリップボードの URL が読み込まれます', + optionalReason: 'ユーザーが同意した場合のみ' + }, + }, }, storage: { + title: 'このデータをどうするか?', p1: 'この拡張機能によって収集されたすべてのデータは、ブラウザのローカル ストレージにのみ保存され、他の場所に配布されることはありません。', p2: 'ただし、拡張機能によって提供されるツールを使用して、データを JSON または CSV ファイル形式でエクスポートまたはインポートできます。 この拡張機能は、GitHub Gist など、データをバックアップするのに十分信頼できるサードパーティ サービスの使用もサポートします。', p3: '私たちはあなたがデータを収集するのを手伝うだけですが、処分する権利はあなたのものでなければなりません.', diff --git a/src/i18n/message/guide/profile.ts b/src/i18n/message/guide/profile.ts deleted file mode 100644 index afa6c876a..000000000 --- a/src/i18n/message/guide/profile.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -type _key = - | 'p1' - | 'p2' - -export type ProfileMessage = { - [key in _key]: string -} - -const _default: Messages = { - zh_CN: { - p1: '{appName}是一款开源、免费、用户友好的,用于统计上网时间的浏览器扩展。您可以在 {github} 上查阅它的源代码。', - p2: '这个页面将会告诉您如何使用它,以及相关的隐私政策。', - }, - zh_TW: { - p1: '{appName}是一款開源、免費、用戶友好的,用於統計上網時間的瀏覽器擴展。您可以在 {github} 上查閱它的源代碼。', - p2: '這個頁面將會告訴您如何使用它,以及相關的隱私政策。', - }, - en: { - p1: '{appName} is a browser extension to track the time you spent on all websites.You can check out its source code on {github}.', - p2: 'This page will tell you how to use it, and the related privacy policy.', - }, - ja: { - p1: '{appName}は、オンラインで費やした時間をカウントするための、オープン ソースで無料のユーザー フレンドリーなブラウザ拡張機能です。 {github} でソース コードを確認できます。 ', - p2: 'このページでは、使用方法と関連するプライバシー ポリシーについて説明します。', - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/start.ts b/src/i18n/message/guide/start.ts new file mode 100644 index 000000000..1c180f2a1 --- /dev/null +++ b/src/i18n/message/guide/start.ts @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +type _Key = + | 'p1' + | 's1' + | 's1p1' + | 's2' + | 's2p1' + | 's3' + | 's3p1' + | 'alert' + +export type StartMessage = { + title: string +} & { + [key in _Key]: string + } + +const _default: Messages = { + zh_CN: { + title: '快速开始', + p1: '只需简单三步,就可以快速开始使用这个扩展。', + s1: '1. 固定扩展的图标', + s1p1: '首先,为了更方便地使用这个扩展,你需要将图标固定到工具栏上。不同浏览器的操作方式不同,下图显示了在 Chrome 中的做法。', + s2: '2. 浏览任何网站', + s2p1: '然后你可以点开任何网站,扩展图标上会展示该网站的当日浏览时间,就像这样 {demo}。', + s3: '3. 在弹出页中查看数据', + s3p1: '最后,点击扩展的图标,你可以在弹出的页面里看到今天、本周以及本月的饼状图数据。', + alert: '你已经学会了基本用法,赶紧试试!!', + }, + en: { + title: 'Get started', + p1: 'You can quickly start using this extension in just 3 easy steps.', + s1: '1. Pin the icon', + s1p1: 'Firstly, to use this extension more conveniently, you\'d better pin the icon to toolbar. It\'s not the same in different browsers to do this, the following figure shows how in Chrome.', + s2: '2. Browse any website', + s2p1: 'Then, browse any website, and you will see that the time is beating on the icon, just like this {demo}.', + s3: '3. Read data in the popup page', + s3p1: 'Finally, click the icon to open the popup page, and you can read the data visualized with pie chart, of today, this week or this month.', + alert: 'You have learned the basic usage, try it!!', + }, + zh_TW: { + title: '快速開始', + p1: '只需簡單三步,就可以快速開始使用這個擴展。', + s1: '1. 固定擴展的圖標', + s1p1: '首先,為了更方便地使用這個擴展,你需要將圖標固定到工具欄上。不同瀏覽器的操作方式不同,下圖顯示了在 Chrome 中的做法。', + s2: '2. 瀏覽任何網站', + s2p1: '然後你可以點開任何網站,擴展圖標上會展示該網站的當日瀏覽時間,就像這樣 {demo}。', + s3: '3. 在彈出頁中查看數據', + s3p1: '最後,點擊擴展的圖標,你可以在彈出的頁面裡看到今天、本週以及本月的餅狀圖數據。', + alert: '你已經學會了基本用法,趕緊試試!!', + }, + ja: { + title: '始めましょう', + p1: 'この拡張機能は、わずか 3 つの簡単な手順ですぐに使い始めることができます。', + s1: '1. アイコンを固定する', + s1p1: 'まず、この拡張機能をより便利に使用するには、アイコンをツールバーにピン留めすることをお勧めします。 これを行う方法はブラウザーによって異なります。次の図は、Chrome での方法を示しています。', + s2: '2. 任意の Web サイトを閲覧する', + s2p1: '次に、任意の Web サイトを閲覧すると、この {demo} のように、時間がアイコンに刻み込まれていることがわかります。', + s3: '3. ポップアップ ページでデータを読み取る', + s3p1: '最後にアイコンをクリックしてポップアップページを開くと、今日、今週、今月のデータを円グラフで可視化して読むことができます。', + alert: '基本的な使い方を学んだので、試してみてください!!', + }, +} + +export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/usage.ts b/src/i18n/message/guide/usage.ts deleted file mode 100644 index 2026a441b..000000000 --- a/src/i18n/message/guide/usage.ts +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2022 Hengyang Zhang - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -type _QuickstartKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - | 'p2' - -type _BackgroundKey = - | 'p1' - | 'l1' - | 'l2' - | 'p2' - | 'backgroundPage' - -type _AdvancedKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - | 'l4' - | 'l5' - | 'l6' - | 'l7' - | 'l8' - -type _BackupKey = - | 'p1' - | 'l1' - | 'l2' - | 'l3' - -export type UsageMessage = { - quickstart: { [key in _QuickstartKey]: string } - background: { [key in _BackgroundKey]: string } - advanced: { [key in _AdvancedKey]: string } - backup: { [key in _BackupKey]: string } -} - -const _default: Messages = { - zh_CN: { - quickstart: { - p1: '首先,您可以通过以下几步,开始体验这个扩展:', - l1: '1. 将扩展的图标固定在浏览器的右上角,具体的操作方法根据您的浏览器而定。该步骤不会影响扩展的正常运行,但是将极大改善您的交互体验。', - l2: '2. 打开任意一个网站,浏览几秒钟,这时您会观察到右上角的图标上有数字跳动,它显示您今天花了多少时间浏览当前网站。', - l3: '3. 点击扩展的图标,会弹出一个页面,展示今天或最近一段时间您的上网数据。', - p2: '需要提醒的是,由于时长数据是实时统计,所以安装扩展之前的浏览记录不会被记录。', - }, - background: { - p1: '基于图标,扩展提供了比较便捷的数据查看方式。但是如果您想要体验它的全部功能,就需要访问扩展的 {background}。进入后台页有以下两种方式:', - l1: '1. 您可以右击扩展的图标,在弹出的菜单中点击【{allFunction}】。', - l2: '2. 您在图标弹出页的下方也可以找到【{allFunction}】链接,同样点击它即可。', - p2: '弹出页和后台页是这个扩展最主要的交互方式,当你知道如何打开他们之后,就可以完整地使用它了。', - backgroundPage: '后台页', - }, - advanced: { - p1: '这个扩展的核心功能是统计您在不同网站上的浏览行为。此外,它也提供了很多高级功能,来满足您更多的需求。当然,所有的功能您都可以在后台页里找到。', - l1: '1. 它可以分析您在一段时间内访问同一个网站的趋势,并以折线图展示。', - l2: '2. 它可以统计您在每天的不同时间段里的上网频率,并以直方图展示。该数据不区分网站,最小的统计粒度为 15 分钟。', - l3: '3. 它可以统计您阅读本地文件的时间,不过该功能需要在选项中启用。', - l4: '4. 它支持白名单功能,您可以将您不想要统计的网站加入白名单。', - l5: '5. 它支持将几个相关的网站合并统计到同一个条目,您可以自定义合并的规则。默认按照 {psl} 合并。', - l6: '6. 它支持限制每个网站的每日浏览时长,需要您手动添加限制规则。', - l7: '7. 它支持夜间模式,同样需要在选项里启用。', - l8: '8. 它支持使用 Github Gist 作为云端存储多个浏览器的数据,并进行聚合查询。需要您准备一个至少包含 gist 权限的 token。', - }, - backup: { - p1: '您可以按以下步骤使用 {gist} 备份您的数据。之后,您可在其他终端上查询已备份数据。', - l1: '1. 首先,您需要在 Github 生成一个包含 gist 权限的 {token}。', - l2: '2. 然后在选项页面将同步方式选为 Github Gist,将你的 token 填入下方出现的输入框中。', - l3: '3. 最后,点击备份按钮即可将本地数据导入到你的 gist 里。', - }, - }, - zh_TW: { - quickstart: { - p1: '首先,您可以通過以下幾步,開始體驗這個擴展:', - l1: '1. 將擴展的圖標固定在瀏覽器的右上角,具體的操作方法根據您的瀏覽器而定。該步驟不會影響擴展的正常運行,但是將極大改善您的交互體驗。', - l2: '2. 打開任意一個網站,瀏覽幾秒鐘,這時您會觀察到右上角的圖標上有數字跳動,它顯示您今天花了多少時間瀏覽當前網站。', - l3: '3. 點擊擴展的圖標,會彈出一個頁面,展示今天或最近一段時間您的上網數據。', - p2: '需要提醒的是,由於時長數據是實時統計,所以安裝擴展之前的瀏覽記錄不會被記錄。', - }, - background: { - p1: '基於圖標,擴展提供了比較便捷的數據查看方式。但是如果您想要體驗它的全部功能,就需要訪問擴展的 {background}。進入後台頁有以下兩種方式:', - l1: '1. 您可以右擊擴展的圖標,在彈出的菜單中點擊【{allFunction}】。', - l2: '2. 您在圖標彈出頁的下方也可以找到【{allFunction}】鏈接,同樣點擊它即可。', - p2: '彈出頁和後台頁是這個擴展最主要的交互方式,當你知道如何打開他們之後,就可以完整地使用它了。', - backgroundPage: '後台頁', - }, - advanced: { - p1: '這個擴展的核心功能是統計您在不同網站上的瀏覽行為。此外,它也提供了很多高級功能,來滿足您更多的需求。當然,所有的功能您都可以在後台頁裡找到。', - l1: '1. 它可以分析您在一段時間內訪問同一個網站的趨勢,並以折線圖展示。', - l2: '2. 它可以統計您在每天的不同時間段裡的上網頻率,並以直方圖展示。該數據不區分網站,最小的統計粒度為 15 分鐘。', - l3: '3. 它可以統計您閱讀本地文件的時間,不過該功能需要在選項中啟用。', - l4: '4. 它支持白名單功能,您可以將您不想要統計的網站加入白名單。', - l5: '5. 它支持將幾個相關的網站合併統計到同一個條目,您可以自定義合併的規則。默認按照 {psl} 合併。', - l6: '6. 它支持限制每個網站的每日瀏覽時長,需要您手動添加限制規則。', - l7: '7. 它支持夜間模式,同樣需要在選項裡啟用。', - l8: '8. 它支持使用 Github Gist 作為雲端存儲多個瀏覽器的數據,並進行聚合查詢。需要您準備一個至少包含 gist 權限的 token。', - }, - backup: { - p1: '您可以按以下步驟使用 {gist} 備份您的數據。之後,您可在其他終端上查詢已備份數據。', - l1: '1. 首先,您需要在 Github 生成一個包含 gist 權限的 {token}。', - l2: '2. 然後在選項頁面將同步方式選為 Github Gist,將你的 token 填入下方出現的輸入框中。', - l3: '3. 最後,點擊備份按鈕即可將本地數據導入到你的 gist 裡。', - }, - }, - en: { - quickstart: { - p1: 'First, you can quickly start using this extension by following these steps:', - l1: '1. Pin the icon of this extension in the upper right corner of the browser. The specific operation method depends on your browser.This step will not affect the normal behavior of it, but will greatly improve your interactive experience.', - l2: '2. Visit any website and browse for a few seconds, then you will observe a number jumping on the icon.it shows how much time you spent today browsing current website', - l3: '3. Click the icon, and a page will pop up, showing your stat data for today or recent days.', - p2: 'It is worth mentioning that since the duration data can only be counted in real time,the history before installation will not be recorded.', - }, - background: { - p1: 'Based on icons, the extension provides a more convenient way to view data.But if you want to experience its full functionality, you need to visit {background} of the extension.There are two ways to enter the background page:', - l1: '1. You can right-click the icon of the extension, and click [{allFunction}] in the pop-up menu.', - l2: '2. You can also find the [{allFunction}] link at the bottom of the icon popup page, just click it.', - p2: 'The popup page and background page are the main interaction methods of this extension. After you know how to open them, you can use it completely.', - backgroundPage: 'the background page', - }, - advanced: { - p1: 'The core function of this extension is to count your browsing behavior on different websites.In addition, it also provides many advanced functions to meet your more needs.Of course, you can find all the functions in the background page.', - l1: '1. It can analyze the trend of your visiting the same website over a period of time, and display it in a line chart.', - l2: '2. It can count your surfing frequency in different time periods every day, and display it in a histogram.The data is site-agnostic and has a minimum statistical granularity of 15 minutes.', - l3: '3. It can count the time you read local files, but this function needs to be enabled in the options.', - l4: '4. It supports the whitelist function, you can add the websites you don\'t want to count to the whitelist.', - l5: '5. It supports merging statistics of several related websites into the same entry, and you can customize the rules for merging. Merge by {psl} by default.', - l6: '6. It supports limiting the daily browsing time of each website, which requires you to manually add limiting rules.', - l7: '7. It supports night mode, which also needs to be enabled in the options.', - l8: '8. It supports using Github Gist as the cloud to store data of multiple browsers and perform aggregated queries. You need to prepare a token with at least gist permission.', - }, - backup: { - p1: 'You can use {gist} to backup your data by following the steps below.Afterwards, you can query the backed up data on other terminals.', - l1: '1. First, you need to generate a {token} with gist permissions on Github.', - l2: '2. Then select Github Gist as the synchronization method on the options page,and fill in your token in the input box that appears below.', - l3: '3. Finally, click the backup button to import the local data into your gist.', - }, - }, - ja: { - quickstart: { - p1: 'まず、次の手順に従って、この拡張機能の調査を開始できます。', - l1: '1. ブラウザの右上隅にある拡張機能のアイコンを修正します。具体的な操作方法はブラウザによって異なります。 この手順は、拡張機能の通常の操作には影響しませんが、インタラクティブなエクスペリエンスを大幅に向上させます。', - l2: '2. 任意の Web サイトを開いて数秒間ブラウジングすると、右上隅のアイコンに数字がジャンプしていることがわかります。これは、現在の Web サイトの閲覧に今日どれだけの時間を費やしたかを示しています。', - l3: '3. 拡張機能のアイコンをクリックすると、ページがポップアップし、今日または最近のインターネット データが表示されます。', - p2: 'なお、継続時間データはリアルタイムでカウントされるため、拡張機能をインストールする前の閲覧履歴は記録されません。', - }, - background: { - p1: 'アイコンに基づいて、拡張機能はデータを表示するためのより便利な方法を提供します。 ただし、完全な機能を体験したい場合は、拡張 {background} にアクセスする必要があります。 バックグラウンド ページに入る方法は 2 つあります。', - l1: '1. 拡張機能のアイコンを右クリックし、ポップアップ メニューで [{allFunction}] をクリックします。', - l2: '2. また、アイコン ポップアップ ページの下部に [{allFunction}] リンクがあり、それをクリックするだけです。', - p2: 'ポップアップ ページと背景ページは、この拡張機能の主な対話方法であり、それらを開く方法を理解すれば、完全に使用できます。', - backgroundPage: '背景ページ', - }, - advanced: { - p1: 'この拡張機能の主な機能は、さまざまな Web サイトでの閲覧行動をカウントすることです。 さらに、より多くのニーズを満たすために多くの高度な機能も提供します。 もちろん、バックグラウンド ページですべての機能を見つけることができます。', - l1: '1. 一定期間の同じ Web サイトへのアクセスの傾向を分析し、折れ線グラフで表示できます。', - l2: '2. あなたのネットサーフィン頻度を毎日異なる時間帯でカウントし、ヒストグラムで表示できます。 データはサイトにとらわれず、最小の統計粒度は 15 分です。', - l3: '3. ローカル ファイルの読み取り時間をカウントできますが、この機能はオプションで有効にする必要があります。', - l4: '4. ホワイトリスト機能をサポートしており、カウントしたくない Web サイトをホワイトリストに追加できます。', - l5: '5. 複数の関連 Web サイトの統計を同じエントリにマージすることをサポートし、マージのルールをカスタマイズできます。 デフォルトでは {psl} でマージします。', - l6: '6. 各 Web サイトの毎日の閲覧時間の制限をサポートしています。これには、制限ルールを手動で追加する必要があります。', - l7: '7.オプションで有効にする必要があるナイトモードをサポートしています。', - l8: '8. Github Gist をクラウドとして使用して、複数のブラウザーのデータを保存し、集約されたクエリを実行することをサポートします。 少なくとも gist 権限を持つトークンを準備する必要があります。', - }, - backup: { - p1: '以下の手順に従って、{gist} を使用してデータをバックアップできます。その後、バックアップされたデータを他の端末で照会できます。', - l1: '1. まず、Github で Gist 権限を持つ {token} を生成する必要があります。', - l2: '2. 次に、オプション ページで同期方法として [Github Gist] を選択し、下に表示される入力ボックスにトークンを入力します。', - l3: '3. 最後に、バックアップ ボタンをクリックして、ローカル データを Gist にインポートします。', - }, - }, -} - -export default _default \ No newline at end of file diff --git a/src/i18n/message/guide/virtual.ts b/src/i18n/message/guide/virtual.ts new file mode 100644 index 000000000..30eaa5945 --- /dev/null +++ b/src/i18n/message/guide/virtual.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export type VirtualMessage = { + title: string + p1: string + step: { + title: string + enter: string + click: string + form: string + browse: string + } +} + +const _default: Messages = { + en: { + title: 'Count data for specific URLs', + p1: 'If you want to count the browsing time and number of visits of certain URLs, not just the domain name, ' + + 'you can do so by creating a virtual site.', + step: { + title: 'Four steps to create a virtual site', + enter: 'First, enter the management page{link}, click the menu item {menuItem}.', + click: 'Click the New button in the upper right corner.', + form: 'Then, fill in the URL to count, Ant expressions is allowed, such as {demo1} or {demo2}, and the name of this virtual site, ' + + 'and then click Save.', + browse: 'Finally, Browse the relevant URLs and observe the data.', + } + }, + zh_CN: { + title: '统计特定 URL 的数据', + p1: '如果你想统计某些 URL ,而不只是域名的浏览时长和访问次数,可以通过创建虚拟站点来实现。', + step: { + title: '简单四步创建一个虚拟站点', + enter: '首先进入管理页{link},点击菜单项【{menuItem}】。', + click: '然后单击右上角的新建按钮。', + form: '填写要统计的 URL,可以使用 Ant 表达式,如 {demo1} 或 {demo2} ,以及这个虚拟站点的名称,然后点击保存。', + browse: '最后浏览相关的网页,观察数据。' + } + }, + zh_TW: { + title: '統計特定 URL 的數據', + p1: '如果你想統計某些 URL ,而不只是域名的瀏覽時長和訪問次數,可以通過創建虛擬站點來實現。', + step: { + title: '簡單四步創建一個虛擬站點', + enter: '首先進入管理頁{link},點擊菜單項【{menuItem}】。', + click: '然後單擊右上角的新建按鈕。', + form: '填寫要統計的 URL,可以使用 Ant 表達式,如 {demo1} 或 {demo2} ,以及這個虛擬站點的名稱,然後點擊保存。', + browse: '最後瀏覽相關的網頁,觀察數據。', + }, + }, + ja: { + title: '特定の URL のデータをカウントする', + p1: 'ドメイン名だけでなく、特定の URL の閲覧時間と訪問回数をカウントしたい場合は、仮想サイトを作成することで実行できます。', + step: { + title: '仮想サイトを作成するための 4 つのステップ', + enter: 'まず、管理ページ{link}に入り、メニュー項目{menuItem}をクリックします。', + click: '右上隅にある [新規] ボタンをクリックします。', + form: '次に、カウントする URL、{demo1} または {demo2} などの Ant 式が許可されていること、およびこの仮想サイトの名前を入力し、[保存] をクリックします。', + browse: '最後に、関連する URL を参照してデータを観察します。', + } + }, +} + +export default _default diff --git a/src/manifest.ts b/src/manifest.ts index 5a47abfd0..d9591bab9 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -14,7 +14,7 @@ // Not use path alias in manifest.json import packageInfo from "./package" import { OPTION_ROUTE } from "./app/router/constants" -const { version, author, homepage } = packageInfo +const { version, author: { name: author }, homepage } = packageInfo const _default: chrome.runtime.ManifestV2 = { name: '__MSG_meta_marketName__', description: "__MSG_meta_description__", diff --git a/src/package.ts b/src/package.ts index c442574d7..20fcd31c5 100644 --- a/src/package.ts +++ b/src/package.ts @@ -19,4 +19,9 @@ const _default: _PackageInfo = { author: packageJson.author } +/** + * @since 1.8.0 + */ +export const AUTHOR_EMAIL: string = packageJson.author.email + export default _default \ No newline at end of file diff --git a/src/service/components/host-merge-ruler.ts b/src/service/components/host-merge-ruler.ts index 70e7ad199..d4a3d30a3 100644 --- a/src/service/components/host-merge-ruler.ts +++ b/src/service/components/host-merge-ruler.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -37,9 +37,12 @@ type RegRuleItem = { result: string | number } -const processRegStr = (regStr: string) => regStr.split('.').join('\\.').split('*').join('[^\\.]+') +const processRegStr = (regStr: string) => regStr + .split('.').join('\\.') + .split('**').join('.+') + .split('*').join('[^\\.]+') -const convert = (dbItem: timer.merge.Rule) => { +function convert(dbItem: timer.merge.Rule): RegRuleItem | [string, string | number] { const { origin, merged } = dbItem if (origin.includes('*')) { const regStr = processRegStr(origin) @@ -57,7 +60,9 @@ export default class CustomizedHostMergeRuler implements timer.merge.Merger { constructor(rules: timer.merge.Rule[]) { rules.map(item => convert(item)) - .forEach(rule => Array.isArray(rule) ? (this.noRegMergeRules[rule[0]] = rule[1]) : (this.regulars.push(rule))) + .forEach(rule => Array.isArray(rule) + ? (this.noRegMergeRules[rule[0]] = rule[1] || rule[0]) + : (this.regulars.push(rule))) } /** diff --git a/src/util/constant/url.ts b/src/util/constant/url.ts index d4e5aaaac..199626a81 100644 --- a/src/util/constant/url.ts +++ b/src/util/constant/url.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Hengyang Zhang + * Copyright (c) 2021-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -90,8 +90,8 @@ export const UPDATE_PAGE = updatePage */ export function getAppPageUrl(isInBackground: boolean, route?: string, query?: any): string { let url = IS_FIREFOX && !isInBackground ? 'app.html' : 'static/app.html' - const queries = query ? Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&') : '' route && (url += '#' + route) + const queries = query ? Object.entries(query).map(([k, v]) => `${k}=${v}`).join('&') : '' queries && (url += '?' + queries) return url } @@ -101,8 +101,10 @@ export function getAppPageUrl(isInBackground: boolean, route?: string, query?: a * @param isInBackground invoke in background environment * @since 1.3.2 */ -export function getGuidePageUrl(isInBackground: boolean): string { - return IS_FIREFOX && !isInBackground ? 'guide.html' : 'static/guide.html' +export function getGuidePageUrl(isInBackground: boolean, route?: string): string { + let url = IS_FIREFOX && !isInBackground ? 'guide.html' : 'static/guide.html' + route && (url += '#' + route) + return url } /** diff --git a/src/util/dark-mode.ts b/src/util/dark-mode.ts index 0b0679305..d3615739d 100644 --- a/src/util/dark-mode.ts +++ b/src/util/dark-mode.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2022 Hengyang Zhang + * Copyright (c) 2022-present Hengyang Zhang * * This software is released under the MIT License. * https://opensource.org/licenses/MIT @@ -25,7 +25,9 @@ function toggle0(isDarkMode: boolean) { * Init from local storage */ export function init() { - toggle0(isDarkMode()) + const val = isDarkMode() + toggle0(val) + return val } export function toggle(isDarkMode: boolean) { diff --git a/src/util/merge.ts b/src/util/merge.ts new file mode 100644 index 000000000..a7d3b8a17 --- /dev/null +++ b/src/util/merge.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2023-present Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import type { MergeCommonMessage } from "@i18n/message/common/merge" + +export type MergeTagType = '' | 'info' | 'success' + +export function computeMergeType(mergedVal: number | string): MergeTagType { + return typeof mergedVal === 'number' ? 'success' : mergedVal === '' ? 'info' : '' +} + +type TypeFinder = (mergeCommon: MergeCommonMessage | EmbeddedPartial) => string + +export function computeMergeTxt( + originVal: string, + mergedVal: number | string, + i18n: (finder: TypeFinder, param?: any) => string +): string { + const mergeTxt = typeof mergedVal === 'number' + ? i18n(msg => msg?.tagResult?.level, { level: mergedVal + 1 }) + : mergedVal === '' ? i18n(msg => msg?.tagResult?.blank) : mergedVal + return `${originVal} >>> ${mergeTxt}` +}