diff --git a/global.d.ts b/global.d.ts index f5552e47c..fd3305f68 100644 --- a/global.d.ts +++ b/global.d.ts @@ -33,6 +33,13 @@ declare namespace Timer { displaySiteName: boolean } + type AppearanceOptionDarkMode = + // Always on + | "on" + // Always off + | "off" + // Timed on + | "timed" type AppearanceOption = { /** * Whether to display the whitelist button in the context menu @@ -58,6 +65,20 @@ declare namespace Timer { * @since 0.8.6 */ printInConsole: boolean + /** + * The state of dark mode + * + * @since 1.1.0 + */ + darkMode: AppearanceOptionDarkMode + + /** + * The range of seconds to turn on dark mode. Required if {@param darkMode} is 'timed' + * + * @since 1.1.0 + */ + darkModeTimeStart?: number + darkModeTimeEnd?: number } type StatisticsOption = { diff --git a/public/popup.html b/public/popup.html index f6967c29c..0af46d1b6 100644 --- a/public/popup.html +++ b/public/popup.html @@ -15,14 +15,18 @@
- +
+ +
- +
+ +
diff --git a/src/app/components/dashboard/components/calendar-heat-map.ts b/src/app/components/dashboard/components/calendar-heat-map.ts index 6f480a1b1..64de1e821 100644 --- a/src/app/components/dashboard/components/calendar-heat-map.ts +++ b/src/app/components/dashboard/components/calendar-heat-map.ts @@ -36,6 +36,7 @@ import { ElLoading } from "element-plus" import { defineComponent, h, onMounted, ref, Ref } from "vue" import { groupBy, rotate } from "@util/array" import { BASE_TITLE_OPTION } from "../common" +import { getPrimaryTextColor } from "@util/style" const WEEK_NUM = 53 @@ -110,6 +111,7 @@ function optionOf(data: _Value[], days: string[]): EcOption { const totalMinutes = data.map(d => d[2] || 0).reduce((a, b) => a + b, 0) const totalHours = Math.floor(totalMinutes / 60) const xAxisLabelMap = getXAxisLabelMap(data) + const textColor = getPrimaryTextColor() return { title: { ...BASE_TITLE_OPTION, @@ -117,7 +119,10 @@ function optionOf(data: _Value[], days: string[]): EcOption { ? msg.dashboard.heatMap.title0 : msg.dashboard.heatMap.title1, { hour: totalHours } - ) + ), + textStyle: { + color: textColor + } }, tooltip: { position: 'top', @@ -137,12 +142,16 @@ function optionOf(data: _Value[], days: string[]): EcOption { formatter: (x: string) => xAxisLabelMap[x] || '', interval: 0, margin: 14, + color: textColor }, }, yAxis: { type: 'category', data: days, - axisLabel: { padding: /* T R B L */[0, 12, 0, 0] }, + axisLabel: { + padding: /* T R B L */[0, 12, 0, 0], + color: textColor + }, axisLine: { show: false }, axisTick: { show: false, alignWithLabel: true } }, @@ -155,7 +164,10 @@ function optionOf(data: _Value[], days: string[]): EcOption { orient: 'vertical', right: '2%', top: 'center', - dimension: 2 + dimension: 2, + textStyle: { + color: textColor + } }], series: [{ name: 'Daily Focus', @@ -163,11 +175,10 @@ function optionOf(data: _Value[], days: string[]): EcOption { data: data.map(d => { let item = { value: d, itemStyle: undefined, label: undefined, emphasis: undefined, tooltip: undefined, silent: false } const minutes = d[2] - const date = d[3] if (minutes) { } else { item.itemStyle = { - color: '#fff', + color: 'transparent', } item.emphasis = { disabled: true diff --git a/src/app/components/dashboard/components/top-k-visit.ts b/src/app/components/dashboard/components/top-k-visit.ts index e693852ab..96dfe2591 100644 --- a/src/app/components/dashboard/components/top-k-visit.ts +++ b/src/app/components/dashboard/components/top-k-visit.ts @@ -8,6 +8,8 @@ import type { ECharts, ComposeOption } from "echarts/core" import type { PieSeriesOption } from "echarts/charts" import type { TitleComponentOption, TooltipComponentOption } from "echarts/components" +import type { Ref } from "vue" +import type { TimerQueryParam } from "@service/timer-service" import { init, use } from "@echarts/core" import PieChart from "@echarts/chart/pie" @@ -16,13 +18,14 @@ import TooltipComponent from "@echarts/component/tooltip" use([PieChart, TitleComponent, TooltipComponent]) -import timerService, { SortDirect, TimerQueryParam } from "@service/timer-service" +import timerService, { SortDirect } from "@service/timer-service" import { MILL_PER_DAY } from "@util/time" import { ElLoading } from "element-plus" -import { defineComponent, h, onMounted, ref, Ref } from "vue" +import { defineComponent, h, onMounted, ref } from "vue" import DataItem from "@entity/dto/data-item" import { BASE_TITLE_OPTION } from "../common" import { t } from "@app/locale" +import { getPrimaryTextColor } from "@util/style" const CONTAINER_ID = '__timer_dashboard_top_k_visit' const TOP_NUM = 6 @@ -40,10 +43,12 @@ type _Value = { } function optionOf(data: _Value[]): EcOption { + const textColor = getPrimaryTextColor() return { title: { ...BASE_TITLE_OPTION, - text: t(msg => msg.dashboard.topK.title, { k: TOP_NUM, day: DAY_NUM }) + text: t(msg => msg.dashboard.topK.title, { k: TOP_NUM, day: DAY_NUM }), + textStyle: { color: textColor } }, tooltip: { show: true, @@ -64,6 +69,7 @@ function optionOf(data: _Value[]): EcOption { itemStyle: { borderRadius: 7 }, + label: { color: textColor }, data: data } } diff --git a/src/app/components/dashboard/components/week-on-week.ts b/src/app/components/dashboard/components/week-on-week.ts index 1c011f30a..8d0aec858 100644 --- a/src/app/components/dashboard/components/week-on-week.ts +++ b/src/app/components/dashboard/components/week-on-week.ts @@ -26,6 +26,7 @@ import DataItem from "@entity/dto/data-item" import { groupBy, sum } from "@util/array" import { BASE_TITLE_OPTION } from "../common" import { t } from "@app/locale" +import { getPrimaryTextColor } from "@util/style" type EcOption = ComposeOption< | CandlestickSeriesOption @@ -48,6 +49,7 @@ type _Value = { } function optionOf(lastPeriodItems: DataItem[], thisPeriodItems: DataItem[]): EcOption { + const textColor = getPrimaryTextColor() const lastPeriodMap: { [host: string]: number } = groupBy(lastPeriodItems, item => item.host, grouped => Math.floor(sum(grouped.map(item => item.focus)) / 1000) @@ -113,15 +115,18 @@ function optionOf(lastPeriodItems: DataItem[], thisPeriodItems: DataItem[]): EcO }, xAxis: { type: 'category', - name: 'Seconds', splitLine: { show: false }, data: topK.map(a => a.host), axisLabel: { - interval: 0 + interval: 0, + color: textColor, }, }, yAxis: { type: 'value', + axisLabel: { + color: textColor, + } }, series: [{ type: 'candlestick', diff --git a/src/app/components/dashboard/feedback.ts b/src/app/components/dashboard/feedback.ts index d1d78b50d..84b8038d9 100644 --- a/src/app/components/dashboard/feedback.ts +++ b/src/app/components/dashboard/feedback.ts @@ -25,7 +25,7 @@ const _default = defineComponent({ }, h(ElTooltip, { placement: 'top', content: t(msg => msg.dashboard.feedback.tooltip), - effect: Effect.LIGHT, + effect: Effect.DARK, }, () => h(ElButton, { type: "info", size: 'small', diff --git a/src/app/components/habit/component/chart/wrapper.ts b/src/app/components/habit/component/chart/wrapper.ts index d59a97354..f6e633ebd 100644 --- a/src/app/components/habit/component/chart/wrapper.ts +++ b/src/app/components/habit/component/chart/wrapper.ts @@ -24,6 +24,7 @@ import { PeriodKey, PERIODS_PER_DATE } from "@entity/dto/period-info" import PeriodResult from "@entity/dto/period-result" import { formatPeriodCommon, formatTime, MILL_PER_DAY } from "@util/time" import { t } from "@app/locale" +import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" type EcOption = ComposeOption< | BarSeriesOption @@ -89,9 +90,12 @@ function generateOptions(data: PeriodResult[], averageByDate: boolean, periodSiz const xAxisMin = periodData[0].startTime.getTime() const xAxisMax = periodData[periodData.length - 1].endTime.getTime() const xAxisAxisLabelFormatter = averageByDate ? '{HH}:{mm}' : formatXAxis + const textColor = getPrimaryTextColor() + const secondaryTextColor = getSecondaryTextColor() return { title: { text: TITLE, + textStyle: { color: textColor }, left: 'center' }, tooltip: { @@ -105,18 +109,26 @@ function generateOptions(data: PeriodResult[], averageByDate: boolean, periodSiz title: t(msg => msg.habit.chart.saveAsImageTitle), name: TITLE, // file name excludeComponents: ['toolbox'], - pixelRatio: 1 + pixelRatio: 1, + iconStyle: { + borderColor: secondaryTextColor + } } } }, xAxis: { - axisLabel: { formatter: xAxisAxisLabelFormatter }, + axisLabel: { formatter: xAxisAxisLabelFormatter, color: textColor }, type: 'time', axisLine: { show: false }, min: xAxisMin, max: xAxisMax }, - yAxis: { name: Y_AXIAS_NAME, type: 'value' }, + yAxis: { + name: Y_AXIAS_NAME, + nameTextStyle: { color: textColor }, + type: 'value', + axisLabel: { color: textColor }, + }, series: [{ type: "bar", large: true, diff --git a/src/app/components/option/common.ts b/src/app/components/option/common.ts index 53059d5a5..57c6faeef 100644 --- a/src/app/components/option/common.ts +++ b/src/app/components/option/common.ts @@ -32,7 +32,7 @@ export function renderOptionItem(input: VNode | { [key: string]: VNode }, label: * @param text text */ export function tagText(text: I18nKey): VNode { - return h('a', { style: { color: '#F56C6C' } }, t(text)) + return h('a', { class: 'option-tag' }, t(text)) } /** diff --git a/src/app/components/option/components/appearance/dark-mode-input.ts b/src/app/components/option/components/appearance/dark-mode-input.ts new file mode 100644 index 000000000..5c45e5be0 --- /dev/null +++ b/src/app/components/option/components/appearance/dark-mode-input.ts @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ +import { Ref, PropType, ComputedRef, watch } from "vue" + +import { ElOption, ElSelect, ElTimePicker } from "element-plus" +import { defineComponent, ref, h, computed } from "vue" +import { t } from "@app/locale" + +function computeSecondToDate(secondOfDate: number): Date { + const now = new Date() + const hour = Math.floor(secondOfDate / 3600) + const minute = Math.floor((secondOfDate - hour * 3600) / 60) + const second = Math.floor(secondOfDate % 60) + now.setHours(hour) + now.setMinutes(minute) + now.setSeconds(second) + now.setMilliseconds(0) + return now +} + +function computeDateToSecond(date: Date) { + const hour = date.getHours() + const minute = date.getMinutes() + const second = date.getSeconds() + return hour * 3600 + minute * 60 + second +} + +const _default = defineComponent({ + name: "DarkModeInput", + props: { + modelValue: String as PropType, + startSecond: Number, + endSecond: Number + }, + emits: ["change"], + setup(props, ctx) { + const darkMode: Ref = ref(props.modelValue) + // @ts-ignore + const start: Ref = ref(computeSecondToDate(props.startSecond)) + // @ts-ignore + const end: Ref = ref(computeSecondToDate(props.endSecond)) + watch(() => props.modelValue, newVal => darkMode.value = newVal) + watch(() => props.startSecond, newVal => start.value = computeSecondToDate(newVal)) + watch(() => props.endSecond, newVal => end.value = computeSecondToDate(newVal)) + const startSecond: ComputedRef = computed(() => computeDateToSecond(start.value)) + const endSecond: ComputedRef = computed(() => computeDateToSecond(end.value)) + + const handleChange = () => ctx.emit("change", darkMode.value, [startSecond.value, endSecond.value]) + + return () => { + const result = [h(ElSelect, { + modelValue: darkMode.value, + size: 'small', + style: { width: '120px', marginLeft: '10px' }, + onChange: async (newVal: string) => { + const before = darkMode.value + darkMode.value = newVal as Timer.AppearanceOptionDarkMode + handleChange() + } + }, { + default: () => ["on", "off", "timed"].map( + value => h(ElOption, { value, label: t(msg => msg.option.appearance.darkMode.options[value]) }) + ) + })] + if (darkMode.value === "timed") { + result.push( + h(ElTimePicker, { + modelValue: start.value, + size: "small", + style: { marginLeft: '10px' }, + "onUpdate:modelValue": (newVal) => { + start.value = newVal + handleChange() + }, + clearable: false + }), + h('a', '-'), + h(ElTimePicker, { + modelValue: end.value, + size: "small", + "onUpdate:modelValue": (newVal) => { + end.value = newVal + handleChange() + }, + clearable: false + }) + ) + } + return result + } + } +}) + +export default _default \ No newline at end of file diff --git a/src/app/components/option/components/appearance.ts b/src/app/components/option/components/appearance/index.ts similarity index 79% rename from src/app/components/option/components/appearance.ts rename to src/app/components/option/components/appearance/index.ts index a30bca466..f80b1173e 100644 --- a/src/app/components/option/components/appearance.ts +++ b/src/app/components/option/components/appearance/index.ts @@ -5,15 +5,19 @@ * https://opensource.org/licenses/MIT */ +import type { Ref } from "vue" + import { ElDivider, ElIcon, ElMessageBox, ElOption, ElSelect, ElSwitch, ElTooltip } from "element-plus" -import { defineComponent, h, Ref, ref } from "vue" +import { defineComponent, h, ref } from "vue" import optionService from "@service/option-service" import { defaultAppearance } from "@util/constant/option" +import DarkModeInput from "./dark-mode-input" import { t, tWith } from "@app/locale" -import { renderOptionItem, tagText } from "../common" +import { renderOptionItem, tagText } from "../../common" import localeMessages from "@util/i18n/components/locale" import { InfoFilled } from "@element-plus/icons-vue" import { localeSameAsBrowser } from "@util/i18n" +import { toggle } from "@util/dark-mode" const displayWhitelist = (option: Ref) => h(ElSwitch, { modelValue: option.value.displayWhitelistMenu, @@ -81,14 +85,35 @@ const _default = defineComponent({ name: "AppearanceOptionContainer", setup(_props, ctx) { const option: Ref = ref(defaultAppearance()) - optionService.getAllOption().then(currentVal => option.value = currentVal) + optionService.getAllOption().then(currentVal => { + option.value = currentVal + console.log(option.value) + }) ctx.expose({ async reset() { option.value = defaultAppearance() await optionService.setAppearanceOption(option.value) + toggle(await optionService.isDarkMode(option.value)) } }) return () => h('div', [ + renderOptionItem({ + input: h(DarkModeInput, { + modelValue: option.value.darkMode, + startSecond: option.value.darkModeTimeStart, + endSecond: option.value.darkModeTimeEnd, + onChange: async (darkMode, range) => { + option.value.darkMode = darkMode + option.value.darkModeTimeStart = range?.[0] + option.value.darkModeTimeEnd = range?.[1] + await optionService.setAppearanceOption(option.value) + toggle(await optionService.isDarkMode()) + } + }) + }, + msg => msg.appearance.darkMode.label, + t(msg => msg.option.appearance.darkMode.options["off"])), + h(ElDivider), renderOptionItem({ input: locale(option), info: h(ElTooltip, {}, { diff --git a/src/app/components/option/style/index.sass b/src/app/components/option/style/index.sass index ed5965ae2..350216e8b 100644 --- a/src/app/components/option/style/index.sass +++ b/src/app/components/option/style/index.sass @@ -23,6 +23,9 @@ margin: 22px 10px 10px 10px font-size: 14px .option-line + display: flex + justify-content: space-between + align-items: center height: 30px line-height: 30px .el-input--small @@ -30,9 +33,29 @@ .el-input__wrapper height: 26px .option-label - float: left + display: inline-flex + align-items: center + color: var(--el-text-color-primary) + i + margin: 0 2px + .el-input-number,.el-select,.el-date-editor--time + margin: 0 3px + .el-select + display: inline-flex + .el-date-editor--time + width: 100px + .el-input__prefix + width: 16px + margin-left: 5px + .el-switch + margin-right: 6px + .option-tag + color: #F56C6C + margin: 0 3px .option-default - float: right + display: inline-flex + align-items: center + color: var(--el-text-color-primary) .el-tag height: 20px .option-container>span diff --git a/src/app/components/report/styles/element.sass b/src/app/components/report/styles/element.sass index 5cd419b38..5d3469f68 100644 --- a/src/app/components/report/styles/element.sass +++ b/src/app/components/report/styles/element.sass @@ -10,8 +10,8 @@ .batch-delete-button margin-right: 20px - height: 41px !important - margin-top: 2px + padding-top: 8px !important + display: inline-flex .export-dropdown @@ -32,7 +32,11 @@ body .el-table th.gutter body .el-table colgroup.gutter display: table-cell!important +$inputHeight: 28px + .el-table__cell .cell + .el-input + height: $inputHeight .edit-btn cursor: pointer line-height: 17px @@ -48,6 +52,9 @@ body .el-table colgroup.gutter margin-left: 0px padding-left: 4px padding-right: 2px - .el-input-group--append .el-input__inner - padding-left: 5px - padding-right: 5px + .el-input-group--append + .el-input__wrapper + height: $inputHeight + .el-input__inner + padding-left: 5px + padding-right: 5px diff --git a/src/app/components/report/table/columns/operation.ts b/src/app/components/report/table/columns/operation.ts index 869c87876..762318696 100644 --- a/src/app/components/report/table/columns/operation.ts +++ b/src/app/components/report/table/columns/operation.ts @@ -60,8 +60,7 @@ const _default = defineComponent({ return () => h(ElTableColumn, { width: width.value, label: columnLabel, - align: "center", - fixed: "right" + align: "center" }, { default: ({ row }: { row: DataItem }) => [ // Trend diff --git a/src/app/components/site-manage/table/column/operation.ts b/src/app/components/site-manage/table/column/operation.ts index 58fef18f4..7c17704da 100644 --- a/src/app/components/site-manage/table/column/operation.ts +++ b/src/app/components/site-manage/table/column/operation.ts @@ -40,7 +40,6 @@ const _default = defineComponent({ minWidth: 100, label, align: 'center', - fixed: 'right' }, { default: ({ row }: { row: HostAliasInfo }) => [ modifyButton(ctx, row), diff --git a/src/app/components/trend/components/chart/wrapper.ts b/src/app/components/trend/components/chart/wrapper.ts index a8054b96b..09ef12cae 100644 --- a/src/app/components/trend/components/chart/wrapper.ts +++ b/src/app/components/trend/components/chart/wrapper.ts @@ -9,6 +9,7 @@ import type { ECharts, ComposeOption } from "echarts/core" import type { LineSeriesOption } from "echarts/charts" import type { GraphicComponentOption, + GridComponentOption, LegendComponentOption, TitleComponentOption, TooltipComponentOption, @@ -30,6 +31,7 @@ import HostOptionInfo from "../../host-option-info" import DataItem from "@entity/dto/data-item" import hostAliasService from "@service/host-alias-service" import HostAlias from "@entity/dao/host-alias" +import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" use([ LineChart, @@ -44,6 +46,7 @@ use([ type EcOption = ComposeOption< | LineSeriesOption | GraphicComponentOption + | GridComponentOption | LegendComponentOption | TitleComponentOption | ToolboxComponentOption @@ -70,10 +73,18 @@ function optionOf( subtext: string, [focusData, totalData, timeData]: number[][] ) { + const textColor = getPrimaryTextColor() + const secondaryTextColor = getSecondaryTextColor() const option: EcOption = { backgroundColor: 'rgba(0,0,0,0)', grid: { top: '100' }, - title: { text: TITLE, subtext, left: 'center' }, + title: { + text: TITLE, + textStyle: { color: textColor }, + subtext, + subtextStyle: { color: secondaryTextColor }, + left: 'center', + }, tooltip: { trigger: 'item' }, toolbox: { feature: { @@ -82,18 +93,36 @@ function optionOf( title: SAVE_AS_IMAGE, excludeComponents: ['toolbox'], pixelRatio: 1, - backgroundColor: '#fff' + backgroundColor: '#fff', + iconStyle: { + borderColor: secondaryTextColor + } } } }, - xAxis: { type: 'category', data: xAxisData }, + xAxis: { + type: 'category', + data: xAxisData, + axisLabel: { color: textColor }, + }, yAxis: [ - { name: TIME_UNIT, type: 'value' }, - { name: NUMBER_UNIT, type: 'value' } + { + name: TIME_UNIT, + nameTextStyle: { color: textColor }, + type: 'value', + axisLabel: { color: textColor }, + }, + { + name: NUMBER_UNIT, + nameTextStyle: { color: textColor }, + type: 'value', + axisLabel: { color: textColor }, + } ], legend: { left: 'left', - data: [t(msg => msg.item.total), t(msg => msg.item.focus), t(msg => msg.item.time)] + data: [t(msg => msg.item.total), t(msg => msg.item.focus), t(msg => msg.item.time)], + textStyle: { color: textColor }, }, series: [{ // run time diff --git a/src/app/index.ts b/src/app/index.ts index 48996b15e..3c8583411 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -5,16 +5,19 @@ * https://opensource.org/licenses/MIT */ -import { App, createApp } from "vue" +import type { App } from "vue" +import type { Language } from "element-plus/lib/locale" + +import { createApp } from "vue" import Main from "./layout" import 'element-plus/theme-chalk/index.css' import './styles' // global css -import './styles/compatible' import installRouter from "./router" import '../common/timer' import ElementPlus from 'element-plus' import { initLocale, locale as appLocale } from "@util/i18n" -import { Language } from "element-plus/lib/locale" +import { toggle, init as initTheme } from "@util/dark-mode" +import optionService from "@service/option-service" const locales: { [locale in Timer.Locale]: () => Promise<{ default: Language }> } = { zh_CN: () => import('element-plus/lib/locale/lang/zh-cn'), @@ -24,6 +27,10 @@ const locales: { [locale in Timer.Locale]: () => Promise<{ default: Language }> } async function main() { + // Init theme with cache first + initTheme() + // Calculate the latest mode + optionService.isDarkMode().then(toggle) await initLocale() const app: App = createApp(Main) installRouter(app) diff --git a/src/app/locale/components/faq.ts b/src/app/locale/components/faq.ts new file mode 100644 index 000000000..e6d1e2310 --- /dev/null +++ b/src/app/locale/components/faq.ts @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +import { Messages } from "@util/i18n" + + +export type FaqMessage = { + usage: { + q: string + a: string + } +} + +const _default: Messages = { + zh_CN: { + usage: { + q: 'Test question', + a: '' + } + }, + zh_TW: { + usage: { + q: '', + a: '' + } + }, + en: { + usage: { + q: '', + a: '' + } + }, + ja: { + usage: { + q: '', + a: '' + } + } +} + +export default _default \ No newline at end of file diff --git a/src/app/locale/components/menu.ts b/src/app/locale/components/menu.ts index 27cd768de..b90b9066e 100644 --- a/src/app/locale/components/menu.ts +++ b/src/app/locale/components/menu.ts @@ -22,6 +22,7 @@ export type MenuMessage = { mergeRule: string option: string other: string + faq: string feedback: string rate: string meat: string @@ -43,6 +44,7 @@ const _default: Messages = { habit: '上网习惯', limit: '每日时限设置', other: '其他', + faq: 'F & Q', feedback: '有什么反馈吗?', rate: '打个分吧!', meat: '请作者吃饭~', @@ -63,6 +65,7 @@ const _default: Messages = { habit: '上網習慣', limit: '每日時限設置', other: '其他', + faq: 'F & Q', feedback: '有什麼反饋嗎?', rate: '打個分吧!', meat: '請作者吃飯~', @@ -82,6 +85,7 @@ const _default: Messages = { whitelist: 'Whitelist', mergeRule: 'Merge-site Rules', other: 'Other Features', + faq: 'F & Q', option: 'Options', feedback: 'Feedback Questionnaire', rate: 'Rate It', @@ -102,6 +106,7 @@ const _default: Messages = { whitelist: 'Webホワイトリスト', mergeRule: 'ドメイン合併', other: 'その他の機能', + faq: 'F & Q', option: '拡張設定', feedback: 'フィードバックアンケート', rate: 'それを評価', diff --git a/src/app/locale/components/option.ts b/src/app/locale/components/option.ts index 176896363..543176fbf 100644 --- a/src/app/locale/components/option.ts +++ b/src/app/locale/components/option.ts @@ -41,6 +41,14 @@ export type OptionMessage = { label: string console: string info: string + }, + darkMode: { + label: string + options: { + on: string + off: string + timed: string + } } } statistics: { @@ -92,6 +100,14 @@ const _default: Messages = { label: '{input} 是否在 {console} 里打印当前网站的 {info}', console: '浏览器的控制台', info: '今日访问信息' + }, + darkMode: { + label: "夜间模式 {input}", + options: { + on: "始终开启", + off: "始终关闭", + timed: "定时开启" + } } }, statistics: { @@ -141,6 +157,14 @@ const _default: Messages = { label: '{input} 是否在 {console} 裡打印當前網站的 {info}', console: '瀏覽器的控製颱', info: '今日訪問信息' + }, + darkMode: { + label: "深色主題 {input}", + options: { + on: "始終開啟", + off: "始終關閉", + timed: "定時開啟" + } } }, statistics: { @@ -190,6 +214,14 @@ const _default: Messages = { label: '{input} Whether to print {info} in the {console}', console: 'console', info: 'the visit count of the current website today' + }, + darkMode: { + label: "Dark mode {info} {input}", + options: { + on: "Always on", + off: "Always off", + timed: "Timed on" + } } }, statistics: { @@ -240,7 +272,15 @@ const _default: Messages = { label: '{input} 現在のウェブサイトの {info} を {console} に印刷するかどうか', console: 'コンソール', info: '今日の情報をご覧ください' - } + }, + darkMode: { + label: "ダークモード {info} {input}", + options: { + on: "常にオン", + off: "常にオフ", + timed: "時限スタート" + } + }, }, statistics: { title: '統計', diff --git a/src/app/locale/messages.ts b/src/app/locale/messages.ts index 694c46706..5c418ed5b 100644 --- a/src/app/locale/messages.ts +++ b/src/app/locale/messages.ts @@ -23,92 +23,92 @@ import dashboardMessages, { DashboardMessage } from "./components/dashboard" import calendarMessages, { CalendarMessage } from "@util/i18n/components/calendar" export type AppMessage = { - dataManage: DataManageMessage - item: ItemMessage - report: ReportMessage - whitelist: WhitelistMessage - mergeRule: MergeRuleMessage - option: OptionMessage - trend: TrendMessage - menu: MenuMessage - habit: HabitMessage - limit: LimitMessage - siteManage: SiteManageMessage - operation: OperationMessage - confirm: ConfirmMessage - dashboard: DashboardMessage - calendar: CalendarMessage + dataManage: DataManageMessage + item: ItemMessage + report: ReportMessage + whitelist: WhitelistMessage + mergeRule: MergeRuleMessage + option: OptionMessage + trend: TrendMessage + menu: MenuMessage + habit: HabitMessage + limit: LimitMessage + siteManage: SiteManageMessage + operation: OperationMessage + confirm: ConfirmMessage + dashboard: DashboardMessage + calendar: CalendarMessage } const _default: Messages = { - zh_CN: { - dataManage: dataManageMessages.zh_CN, - item: itemMessages.zh_CN, - report: reportMessages.zh_CN, - whitelist: whitelistMessages.zh_CN, - mergeRule: mergeRuleMessages.zh_CN, - option: optionMessages.zh_CN, - trend: trendMessages.zh_CN, - menu: menuMessages.zh_CN, - habit: habitMessages.zh_CN, - limit: limitMessages.zh_CN, - siteManage: siteManageManages.zh_CN, - operation: operationMessages.zh_CN, - confirm: confirmMessages.zh_CN, - dashboard: dashboardMessages.zh_CN, - calendar: calendarMessages.zh_CN, - }, - zh_TW: { - dataManage: dataManageMessages.zh_TW, - item: itemMessages.zh_TW, - report: reportMessages.zh_TW, - whitelist: whitelistMessages.zh_TW, - mergeRule: mergeRuleMessages.zh_TW, - option: optionMessages.zh_TW, - trend: trendMessages.zh_TW, - menu: menuMessages.zh_TW, - habit: habitMessages.zh_TW, - limit: limitMessages.zh_TW, - siteManage: siteManageManages.zh_TW, - operation: operationMessages.zh_TW, - confirm: confirmMessages.zh_TW, - dashboard: dashboardMessages.zh_TW, - calendar: calendarMessages.zh_TW, - }, - en: { - dataManage: dataManageMessages.en, - item: itemMessages.en, - report: reportMessages.en, - whitelist: whitelistMessages.en, - mergeRule: mergeRuleMessages.en, - option: optionMessages.en, - trend: trendMessages.en, - menu: menuMessages.en, - habit: habitMessages.en, - limit: limitMessages.en, - siteManage: siteManageManages.en, - operation: operationMessages.en, - confirm: confirmMessages.en, - dashboard: dashboardMessages.en, - calendar: calendarMessages.en, - }, - ja: { - dataManage: dataManageMessages.ja, - item: itemMessages.ja, - report: reportMessages.ja, - whitelist: whitelistMessages.ja, - mergeRule: mergeRuleMessages.ja, - option: optionMessages.ja, - trend: trendMessages.ja, - menu: menuMessages.ja, - habit: habitMessages.ja, - limit: limitMessages.ja, - siteManage: siteManageManages.ja, - operation: operationMessages.ja, - confirm: confirmMessages.ja, - dashboard: dashboardMessages.ja, - calendar: calendarMessages.ja, - } + zh_CN: { + dataManage: dataManageMessages.zh_CN, + item: itemMessages.zh_CN, + report: reportMessages.zh_CN, + whitelist: whitelistMessages.zh_CN, + mergeRule: mergeRuleMessages.zh_CN, + option: optionMessages.zh_CN, + trend: trendMessages.zh_CN, + menu: menuMessages.zh_CN, + habit: habitMessages.zh_CN, + limit: limitMessages.zh_CN, + siteManage: siteManageManages.zh_CN, + operation: operationMessages.zh_CN, + confirm: confirmMessages.zh_CN, + dashboard: dashboardMessages.zh_CN, + calendar: calendarMessages.zh_CN, + }, + zh_TW: { + dataManage: dataManageMessages.zh_TW, + item: itemMessages.zh_TW, + report: reportMessages.zh_TW, + whitelist: whitelistMessages.zh_TW, + mergeRule: mergeRuleMessages.zh_TW, + option: optionMessages.zh_TW, + trend: trendMessages.zh_TW, + menu: menuMessages.zh_TW, + habit: habitMessages.zh_TW, + limit: limitMessages.zh_TW, + siteManage: siteManageManages.zh_TW, + operation: operationMessages.zh_TW, + confirm: confirmMessages.zh_TW, + dashboard: dashboardMessages.zh_TW, + calendar: calendarMessages.zh_TW, + }, + en: { + dataManage: dataManageMessages.en, + item: itemMessages.en, + report: reportMessages.en, + whitelist: whitelistMessages.en, + mergeRule: mergeRuleMessages.en, + option: optionMessages.en, + trend: trendMessages.en, + menu: menuMessages.en, + habit: habitMessages.en, + limit: limitMessages.en, + siteManage: siteManageManages.en, + operation: operationMessages.en, + confirm: confirmMessages.en, + dashboard: dashboardMessages.en, + calendar: calendarMessages.en, + }, + ja: { + dataManage: dataManageMessages.ja, + item: itemMessages.ja, + report: reportMessages.ja, + whitelist: whitelistMessages.ja, + mergeRule: mergeRuleMessages.ja, + option: optionMessages.ja, + trend: trendMessages.ja, + menu: menuMessages.ja, + habit: habitMessages.ja, + limit: limitMessages.ja, + siteManage: siteManageManages.ja, + operation: operationMessages.ja, + confirm: confirmMessages.ja, + dashboard: dashboardMessages.ja, + calendar: calendarMessages.ja, + } } export default _default \ No newline at end of file diff --git a/src/app/router/index.ts b/src/app/router/index.ts index 9ebb79213..b28636ca7 100644 --- a/src/app/router/index.ts +++ b/src/app/router/index.ts @@ -5,8 +5,10 @@ * https://opensource.org/licenses/MIT */ -import { App } from "vue" -import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router" +import type { App } from "vue" +import type { RouteRecordRaw } from "vue-router" + +import { createRouter, createWebHashHistory } from "vue-router" import { OPTION_ROUTE, TREND_ROUTE, LIMIT_ROUTE, REPORT_ROUTE } from "./constants" import metaService from "@service/meta-service" @@ -64,11 +66,14 @@ const additionalRoutes: RouteRecordRaw[] = [ } ] +const otherRoutes: RouteRecordRaw[] = [] + const routes: RouteRecordRaw[] = [ { path: '/', redirect: '/data' }, ...dataRoutes, ...behaviorRoutes, - ...additionalRoutes + ...additionalRoutes, + ...otherRoutes, ] const router = createRouter({ diff --git a/src/app/styles/compatible.sass b/src/app/styles/compatible.sass index 3ce165107..03df3b636 100644 --- a/src/app/styles/compatible.sass +++ b/src/app/styles/compatible.sass @@ -5,12 +5,11 @@ * https://opensource.org/licenses/MIT */ -// Compatible for element-ui +// Compatible element-plus 2.x.x for element-ui + .el-button--small min-height: 28px padding: 7px 15px -.el-input - line-height: var(--el-input-height) .el-input__wrapper height: var(--el-input-inner-height) diff --git a/src/app/styles/dark-theme.sass b/src/app/styles/dark-theme.sass new file mode 100644 index 000000000..8420bc3fb --- /dev/null +++ b/src/app/styles/dark-theme.sass @@ -0,0 +1,78 @@ +html[data-theme='dark']:root + --el-color-primary: #409eff + --el-color-primary-light-3: #3375b9 + --el-color-primary-light-5: #2a598a + --el-color-primary-light-7: #213d5b + --el-color-primary-light-8: #1d3043 + --el-color-primary-light-9: #18222c + --el-color-primary-dark-2: #66b1ff + --el-color-success: #67c23a + --el-color-success-light-3: #4e8e2f + --el-color-success-light-5: #3e6b27 + --el-color-success-light-7: #2d481f + --el-color-success-light-8: #25371c + --el-color-success-light-9: #1c2518 + --el-color-success-dark-2: #85ce61 + --el-color-warning: #e6a23c + --el-color-warning-light-3: #a77730 + --el-color-warning-light-5: #7d5b28 + --el-color-warning-light-7: #533f20 + --el-color-warning-light-8: #3e301c + --el-color-warning-light-9: #292218 + --el-color-warning-dark-2: #ebb563 + --el-color-danger: #f56c6c + --el-color-danger-light-3: #b25252 + --el-color-danger-light-5: #854040 + --el-color-danger-light-7: #582e2e + --el-color-danger-light-8: #412626 + --el-color-danger-light-9: #2b1d1d + --el-color-danger-dark-2: #f78989 + --el-color-error: #f56c6c + --el-color-error-light-3: #b25252 + --el-color-error-light-5: #854040 + --el-color-error-light-7: #582e2e + --el-color-error-light-8: #412626 + --el-color-error-light-9: #2b1d1d + --el-color-error-dark-2: #f78989 + --el-color-info: #909399 + --el-color-info-light-3: #6b6d71 + --el-color-info-light-5: #525457 + --el-color-info-light-7: #393a3c + --el-color-info-light-8: #2d2d2f + --el-color-info-light-9: #202121 + --el-color-info-dark-2: #a6a9ad + --el-box-shadow: 0px 12px 32px 4px rgba(0 0 0 .36) 0px 8px 20px rgba(0 0 0 .72) + --el-box-shadow-light: 0px 0px 12px rgba(0 0 0 .72) + --el-box-shadow-lighter: 0px 0px 6px rgba(0 0 0 .72) + --el-box-shadow-dark: 0px 16px 48px 16px rgba(0 0 0 .72) 0px 12px 32px #000000 0px 8px 16px -8px #000000 + --el-bg-color-page: #0a0a0a + --el-bg-color: #141414 + --el-bg-color-overlay: #1d1e1f + --el-text-color-primary: #E5EAF3 + --el-text-color-regular: #CFD3DC + --el-text-color-secondary: #A3A6AD + --el-text-color-placeholder: #8D9095 + --el-text-color-disabled: #6C6E72 + --el-border-color-darker: #636466 + --el-border-color-dark: #58585B + --el-border-color: #4C4D4F + --el-border-color-light: #414243 + --el-border-color-lighter: #363637 + --el-border-color-extra-light: #2B2B2C + --el-fill-color-darker: #424243 + --el-fill-color-dark: #39393A + --el-fill-color: #303030 + --el-fill-color-light: #262727 + --el-fill-color-lighter: #1D1D1D + --el-fill-color-extra-light: #191919 + --el-fill-color-blank: var(--el-fill-color-darker) + --el-mask-color: rgba(0 0 0 .8) + --el-mask-color-extra-light: rgba(0 0 0 .3) + // timer + --timer-app-container-bg-color: var(--el-fill-color-dark) + // element-plus + .el-switch__core .el-switch__action + background-color: var(--el-fill-color-darker) + // menu + --el-menu-bg-color: var(--el-fill-color-light) + --el-menu-item-active-bg-color: var(--el-color-primary-light-5) diff --git a/src/app/styles/index.sass b/src/app/styles/index.sass index f958d6f42..35cd16851 100644 --- a/src/app/styles/index.sass +++ b/src/app/styles/index.sass @@ -5,6 +5,8 @@ * https://opensource.org/licenses/MIT */ +@import "./dark-theme" +@import "./light-theme" @import "./compatible" body @@ -34,6 +36,7 @@ a .app-container width: 100% + background: var(--timer-app-container-bg-color) margin: auto .content-container @@ -51,6 +54,8 @@ a overflow-y: auto .filter-container + display: flex + align-items: center user-select: none .el-card__body padding-bottom: 10px @@ -62,6 +67,7 @@ a margin-right: 4px .filter-item + display: inline-flex padding-right: 20px .el-input__suffix @@ -84,7 +90,7 @@ a align-items: center .filter-name display: inline-flex - color: #909399 + color: var(--el-text-color-secondary) font-weight: bold font-size: 14px pointer-events: none @@ -117,8 +123,6 @@ a .el-menu height: 100% - -.el-menu border: none padding-top: 10px @@ -128,13 +132,9 @@ a font-size: 18px !important .el-menu-item.is-active - background: #0a6cfa + background: var(--el-menu-item-active-bg-color) \:root - // el-menu - --el-menu-bg-color: rgb(29, 34, 45) // el-menu-item --el-menu-item-height: 48px - --el-menu-text-color: #c1c6c8 --el-menu-active-color: var(--el-menu-text-color) - --el-menu-hover-bg-color: #262f3e diff --git a/src/app/styles/light-theme.sass b/src/app/styles/light-theme.sass new file mode 100644 index 000000000..f17f4a37e --- /dev/null +++ b/src/app/styles/light-theme.sass @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2021 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +\:root + --el-menu-bg-color: #1d222d + --timer-app-container-bg-color: var(--el-fill-color-blank) + --el-menu-text-color: #c1c6c8 + --el-menu-item-active-bg-color: #0a6cfa + --el-menu-hover-bg-color: #262f3e diff --git a/src/popup/components/chart/option.ts b/src/popup/components/chart/option.ts index 22ffbacb0..00dbee8b0 100644 --- a/src/popup/components/chart/option.ts +++ b/src/popup/components/chart/option.ts @@ -18,6 +18,7 @@ import type QueryResult from "@popup/common/query-result" import DataItem from "@entity/dto/data-item" import { formatPeriodCommon, formatTime } from "@util/time" import { t } from "@popup/locale" +import { getPrimaryTextColor, getSecondaryTextColor } from "@util/style" type EcOption = ComposeOption< | PieSeriesOption @@ -62,13 +63,6 @@ const staticOptions: EcOption = { tooltip: { trigger: 'item' }, - legend: { - type: 'scroll', - orient: 'vertical', - left: 15, - top: 20, - bottom: 20, - }, series: [{ name: "Wasted Time", type: "pie", @@ -151,22 +145,34 @@ export function pieOptions(props: PipProps, container: HTMLDivElement): EcOption const { type, mergeHost, data, displaySiteName, chartTitle, date } = props const titleText = chartTitle const subTitleText = `${calculateSubTitleText(date)} @ ${app}` + const textColor = getPrimaryTextColor() + const secondaryColor = getSecondaryTextColor() const options: EcOption = { title: { text: titleText, subtext: subTitleText, - left: 'center' + left: 'center', + textStyle: { color: textColor }, + subtextStyle: { color: secondaryColor }, }, tooltip: { ...staticOptions.tooltip, formatter: params => toolTipFormatter(props, params), position: (point: (number | string)[]) => calcPositionOfTooltip(container, point) }, - legend: staticOptions.legend, + legend: { + type: 'scroll', + orient: 'vertical', + left: 15, + top: 20, + bottom: 20, + textStyle: { color: secondaryColor } + }, series: [{ ...staticOptions.series[0], label: { - formatter: ({ name }) => mergeHost || name === t(msg => msg.otherLabel) ? name : `{${legend2LabelStyle(name)}|} {a|${name}}` + formatter: ({ name }) => mergeHost || name === t(msg => msg.otherLabel) ? name : `{${legend2LabelStyle(name)}|} {a|${name}}`, + color: textColor } }], toolbox: staticOptions.toolbox diff --git a/src/popup/index.ts b/src/popup/index.ts index 0388c3902..f4e533dee 100644 --- a/src/popup/index.ts +++ b/src/popup/index.ts @@ -10,6 +10,12 @@ import renderChart, { handleRestore } from "./components/chart" import initFooter, { queryInfo } from "./components/footer" import metaService from "@service/meta-service" import "../common/timer" +import { toggle, init as initTheme } from "@util/dark-mode" +import optionService from "@service/option-service" + +// Calculate the latest mode +initTheme() +optionService.isDarkMode().then(toggle) handleRestore(queryInfo) initFooter(renderChart) diff --git a/src/popup/style/dark-theme.sass b/src/popup/style/dark-theme.sass new file mode 100644 index 000000000..694a65447 --- /dev/null +++ b/src/popup/style/dark-theme.sass @@ -0,0 +1,22 @@ +html[data-theme='dark']:root + --el-bg-color: #141414 + --el-bg-color-overlay: #1d1e1f + --el-fill-color-darker: #424243 + --el-fill-color-dark: #39393A + --el-fill-color-light: #262727 + --el-fill-color-lighter: #1D1D1D + --el-text-color-primary: #E5EAF3 + --el-text-color-regular: #CFD3DC + --el-text-color-secondary: #A3A6AD + --el-text-color-placeholder: #8D9095 + --el-border-color-darker: #636466 + --el-border-color-dark: #58585B + --el-border-color: #4C4D4F + --el-border-color-light: #414243 + --el-border-color-lighter: #363637 + --el-border-color-extra-light: #2B2B2C + + --el-bg-color: var(--el-fill-color-dark) + --el-fill-color-blank: var(--el-fill-color-darker) + .el-switch__core .el-switch__action + background-color: var(--el-fill-color-darker) diff --git a/src/popup/style/index.sass b/src/popup/style/index.sass index d86795fac..ced23dc43 100644 --- a/src/popup/style/index.sass +++ b/src/popup/style/index.sass @@ -12,12 +12,18 @@ @import url("element-plus/theme-chalk/el-popper.css") @import url("element-plus/theme-chalk/el-option.css") @import url("element-plus/theme-chalk/el-icon.css") +@import ./dark-theme $width: 750px $height: 500px $footerHeight: 40px +body + margin: 0 + background-color: var(--el-bg-color) + #app + padding: 8px width: $width height: calc($height + $footerHeight) @@ -32,7 +38,7 @@ $optionPadding: 10px margin: auto height: calc($footerHeight - $optionPadding) #total-info - color: rgb(96, 98, 102) + color: var(--el-text-color-secondary) font-size: 12px .option-right diff --git a/src/service/option-service.ts b/src/service/option-service.ts index 156d290aa..8e9bdf3b9 100644 --- a/src/service/option-service.ts +++ b/src/service/option-service.ts @@ -41,12 +41,43 @@ async function setOption(option: Partial): Promise { await db.setOption(toSet) } +async function isDarkMode(targetVal?: Timer.AppearanceOption): Promise { + const option = targetVal || await getAllOption() + const darkMode = option.darkMode + if (darkMode === "on") { + return true + } else if (darkMode === "off") { + return false + } else if (darkMode === "timed") { + const start = option.darkModeTimeStart + const end = option.darkModeTimeEnd + if (start === undefined || end === undefined) { + return false + } + const now = new Date() + const currentSecs = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds() + if (start > end) { + // Mostly + return start <= currentSecs || currentSecs <= end + } else if (start < end) { + return start <= currentSecs && currentSecs <= end + } else { + return currentSecs === start + } + } + return false +} + class OptionService { getAllOption = getAllOption setPopupOption = setPopupOption setAppearanceOption = setAppearanceOption setStatisticsOption = setStatisticsOption addOptionChangeListener = db.addOptionChangeListener + /** + * @since 1.1.0 + */ + isDarkMode = isDarkMode } export default new OptionService() \ No newline at end of file diff --git a/src/util/constant/option.ts b/src/util/constant/option.ts index 5d687f93e..69de4d0c0 100644 --- a/src/util/constant/option.ts +++ b/src/util/constant/option.ts @@ -24,7 +24,13 @@ export function defaultAppearance(): Timer.AppearanceOption { // Change false to true @since 0.8.4 displayBadgeText: true, locale: "default", - printInConsole: true + printInConsole: true, + darkMode: "off", + // 6 PM - 6 AM + // 18*60*60 + darkModeTimeStart: 64800, + // 6*60*60 + darkModeTimeEnd: 21600, } } diff --git a/src/util/dark-mode.ts b/src/util/dark-mode.ts new file mode 100644 index 000000000..b15f86f87 --- /dev/null +++ b/src/util/dark-mode.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +/** + * Dark mode + * + * @since 1.1.0 + */ + +const THEME_ATTR = "data-theme" +const DARK_VAL = "dark" +const STORAGE_KEY = "isDark" +const STORAGE_FLAG = "1" + +function toggle0(isDarkMode: boolean) { + const htmlEl = document.getElementsByTagName("html")?.[0] + htmlEl.setAttribute(THEME_ATTR, isDarkMode ? DARK_VAL : "") +} + +/** + * Init from local storage + */ +export function init() { + toggle0(isDarkMode()) +} + +export function toggle(isDarkMode: boolean) { + toggle0(isDarkMode) + localStorage.setItem(STORAGE_KEY, isDarkMode ? STORAGE_FLAG : undefined) +} + +export function isDarkMode() { + return localStorage.getItem(STORAGE_KEY) === STORAGE_FLAG +} \ No newline at end of file diff --git a/src/util/style.ts b/src/util/style.ts new file mode 100644 index 000000000..439a88485 --- /dev/null +++ b/src/util/style.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) 2022 Hengyang Zhang + * + * This software is released under the MIT License. + * https://opensource.org/licenses/MIT + */ + +export function getCssVariable(varName: string, ele?: HTMLElement): string { + const realEle = ele || document.documentElement + if (!realEle) { + return undefined + } + return getComputedStyle(ele || document.documentElement).getPropertyValue(varName) +} + +export function getPrimaryTextColor(): string { + return getCssVariable("--el-text-color-primary") +} + +export function getSecondaryTextColor(): string { + return getCssVariable("--el-text-color-secondary") +} \ No newline at end of file