Merge pull request #465 from DavidHenryThoreau/css

Add colors-dark.scss
This commit is contained in:
Sam 2023-12-20 11:01:55 +01:00 committed by GitHub
commit d207beb78f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 1057 additions and 507 deletions

View File

@ -7,11 +7,11 @@
<link rel="stylesheet" href="/static/css/fork-awesome.min.css"/> <link rel="stylesheet" href="/static/css/fork-awesome.min.css"/>
<link rel="stylesheet" href="/static/css/leaflet.css"/> <link rel="stylesheet" href="/static/css/leaflet.css"/>
<title>FitTrackee</title> <title>FitTrackee</title>
<script type="module" crossorigin src="/static/index-0EDbp-Wc.js"></script> <script type="module" crossorigin src="/static/index-0v1Cinoq.js"></script>
<link rel="modulepreload" crossorigin href="/static/charts-_RwsDDkL.js"> <link rel="modulepreload" crossorigin href="/static/charts-_RwsDDkL.js">
<link rel="modulepreload" crossorigin href="/static/maps-ZyuCPqes.js"> <link rel="modulepreload" crossorigin href="/static/maps-ZyuCPqes.js">
<link rel="stylesheet" crossorigin href="/static/css/maps-B7qTrBCW.css"> <link rel="stylesheet" crossorigin href="/static/css/maps-B7qTrBCW.css">
<link rel="stylesheet" crossorigin href="/static/css/index-kyYSvvW8.css"> <link rel="stylesheet" crossorigin href="/static/css/index-86bdJKFy.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

457
fittrackee/dist/static/index-0v1Cinoq.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,34 @@
"""add dark theme preferences
Revision ID: 14f48e46f320
Revises: 24eb097614e4
Create Date: 2023-12-16 18:35:31.377007
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '14f48e46f320'
down_revision = '24eb097614e4'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.add_column(
sa.Column('use_dark_mode', sa.Boolean(), nullable=True)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('users', schema=None) as batch_op:
batch_op.drop_column('use_dark_mode')
# ### end Alembic commands ###

View File

@ -1459,6 +1459,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
imperial_units=True, imperial_units=True,
display_ascent=False, display_ascent=False,
start_elevation_at_zero=False, start_elevation_at_zero=False,
use_dark_mode=True,
use_raw_gpx_speed=True, use_raw_gpx_speed=True,
date_format='yyyy-MM-dd', date_format='yyyy-MM-dd',
) )
@ -1478,6 +1479,7 @@ class TestUserPreferencesUpdate(ApiTestCaseMixin):
assert data['data']['timezone'] == 'America/New_York' assert data['data']['timezone'] == 'America/New_York'
assert data['data']['date_format'] == 'yyyy-MM-dd' assert data['data']['date_format'] == 'yyyy-MM-dd'
assert data['data']['weekm'] is True assert data['data']['weekm'] is True
assert data['data']['use_dark_mode'] is True
@pytest.mark.parametrize( @pytest.mark.parametrize(
'client_scope, can_access', 'client_scope, can_access',

View File

@ -77,6 +77,12 @@ class TestUserSerializeAsAuthUser(UserModelAssertMixin):
assert serialized_user['timezone'] == user_1.timezone assert serialized_user['timezone'] == user_1.timezone
assert serialized_user['weekm'] == user_1.weekm assert serialized_user['weekm'] == user_1.weekm
assert serialized_user['display_ascent'] == user_1.display_ascent assert serialized_user['display_ascent'] == user_1.display_ascent
assert (
serialized_user['start_elevation_at_zero']
== user_1.start_elevation_at_zero
)
assert serialized_user['use_raw_gpx_speed'] == user_1.use_raw_gpx_speed
assert serialized_user['use_dark_mode'] == user_1.use_dark_mode
def test_it_returns_workouts_infos(self, app: Flask, user_1: User) -> None: def test_it_returns_workouts_infos(self, app: Flask, user_1: User) -> None:
serialized_user = user_1.serialize(user_1) serialized_user = user_1.serialize(user_1)
@ -155,6 +161,9 @@ class TestUserSerializeAsAdmin(UserModelAssertMixin):
assert 'language' not in serialized_user assert 'language' not in serialized_user
assert 'timezone' not in serialized_user assert 'timezone' not in serialized_user
assert 'weekm' not in serialized_user assert 'weekm' not in serialized_user
assert 'start_elevation_at_zero' not in serialized_user
assert 'use_raw_gpx_speed' not in serialized_user
assert 'use_dark_mode' not in serialized_user
def test_it_returns_workouts_infos( def test_it_returns_workouts_infos(
self, app: Flask, user_1_admin: User, user_2: User self, app: Flask, user_1_admin: User, user_2: User

View File

@ -361,6 +361,7 @@ def get_authenticated_user_profile(
"total_ascent": 720.35, "total_ascent": 720.35,
"total_distance": 67.895, "total_distance": 67.895,
"total_duration": "6:50:27", "total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": false, "use_raw_gpx_speed": false,
"username": "sam", "username": "sam",
"weekm": false "weekm": false
@ -478,6 +479,7 @@ def edit_user(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35, "total_ascent": 720.35,
"total_distance": 67.895, "total_distance": 67.895,
"total_duration": "6:50:27", "total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": false, "use_raw_gpx_speed": false,
"username": "sam" "username": "sam"
"weekm": true, "weekm": true,
@ -650,6 +652,7 @@ def update_user_account(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35, "total_ascent": 720.35,
"total_distance": 67.895, "total_distance": 67.895,
"total_duration": "6:50:27", "total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": false, "use_raw_gpx_speed": false,
"username": "sam" "username": "sam"
"weekm": true, "weekm": true,
@ -878,6 +881,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
"total_ascent": 720.35, "total_ascent": 720.35,
"total_distance": 67.895, "total_distance": 67.895,
"total_duration": "6:50:27", "total_duration": "6:50:27",
"use_dark_mode": null,
"use_raw_gpx_speed": true, "use_raw_gpx_speed": true,
"username": "sam" "username": "sam"
"weekm": true, "weekm": true,
@ -892,6 +896,8 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
:<json string language: language preferences :<json string language: language preferences
:<json boolean start_elevation_at_zero: do elevation plots start at zero? :<json boolean start_elevation_at_zero: do elevation plots start at zero?
:<json string timezone: user time zone :<json string timezone: user time zone
:<json boolean use_dark_mode: Display interface with dark mode if true.
If null, it uses browser preferences.
:<json boolean use_raw_gpx_speed: Use unfiltered gpx to calculate speeds :<json boolean use_raw_gpx_speed: Use unfiltered gpx to calculate speeds
:<json boolean weekm: does week start on Monday? :<json boolean weekm: does week start on Monday?
@ -916,6 +922,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
'language', 'language',
'start_elevation_at_zero', 'start_elevation_at_zero',
'timezone', 'timezone',
'use_dark_mode',
'use_raw_gpx_speed', 'use_raw_gpx_speed',
'weekm', 'weekm',
} }
@ -928,6 +935,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
language = get_language(post_data.get('language')) language = get_language(post_data.get('language'))
start_elevation_at_zero = post_data.get('start_elevation_at_zero') start_elevation_at_zero = post_data.get('start_elevation_at_zero')
use_raw_gpx_speed = post_data.get('use_raw_gpx_speed') use_raw_gpx_speed = post_data.get('use_raw_gpx_speed')
use_dark_mode = post_data.get('use_dark_mode')
timezone = post_data.get('timezone') timezone = post_data.get('timezone')
weekm = post_data.get('weekm') weekm = post_data.get('weekm')
@ -938,6 +946,7 @@ def edit_user_preferences(auth_user: User) -> Union[Dict, HttpResponse]:
auth_user.language = language auth_user.language = language
auth_user.start_elevation_at_zero = start_elevation_at_zero auth_user.start_elevation_at_zero = start_elevation_at_zero
auth_user.timezone = timezone auth_user.timezone = timezone
auth_user.use_dark_mode = use_dark_mode
auth_user.use_raw_gpx_speed = use_raw_gpx_speed auth_user.use_raw_gpx_speed = use_raw_gpx_speed
auth_user.weekm = weekm auth_user.weekm = weekm
db.session.commit() db.session.commit()

View File

@ -63,6 +63,7 @@ class User(BaseModel):
db.Boolean, default=True, nullable=False db.Boolean, default=True, nullable=False
) )
use_raw_gpx_speed = db.Column(db.Boolean, default=False, nullable=False) use_raw_gpx_speed = db.Column(db.Boolean, default=False, nullable=False)
use_dark_mode = db.Column(db.Boolean, default=False, nullable=True)
def __repr__(self) -> str: def __repr__(self) -> str:
return f'<User {self.username!r}>' return f'<User {self.username!r}>'
@ -217,6 +218,7 @@ class User(BaseModel):
'language': self.language, 'language': self.language,
'start_elevation_at_zero': self.start_elevation_at_zero, 'start_elevation_at_zero': self.start_elevation_at_zero,
'timezone': self.timezone, 'timezone': self.timezone,
'use_dark_mode': self.use_dark_mode,
'use_raw_gpx_speed': self.use_raw_gpx_speed, 'use_raw_gpx_speed': self.use_raw_gpx_speed,
'weekm': self.weekm, 'weekm': self.weekm,
}, },

View File

@ -123,7 +123,7 @@
.scroll-button { .scroll-button {
background-color: var(--scroll-button-bg-color); background-color: var(--scroll-button-bg-color);
border-radius: $border-radius; border-radius: $border-radius;
box-shadow: 1px 1px 3px lightgrey; box-shadow: 1px 1px 3px var(--app-shadow-color);
display: none; display: none;
padding: 0 $default-padding; padding: 0 $default-padding;

View File

@ -70,14 +70,14 @@
.dropdown-list { .dropdown-list {
list-style-type: none; list-style-type: none;
background-color: #ffffff; background-color: var(--dropdown-background-color);
padding: 0 !important; padding: 0 !important;
margin-top: 5px; margin-top: 5px;
margin-left: -20px !important; margin-left: -20px !important;
position: absolute; position: absolute;
text-align: left; text-align: left;
border: solid 1px lightgrey; border: solid 1px var(--dropdown-border-color);
box-shadow: 2px 2px 5px lightgrey; box-shadow: 2px 2px 5px var(--app-shadow-color);
width: auto !important; width: auto !important;
.dropdown-item { .dropdown-item {

View File

@ -51,6 +51,12 @@
width: 400px; width: 400px;
height: 225px; height: 225px;
z-index: 100; z-index: 100;
filter: var(--map-display-hover-filter);
.map-attribution-text {
color: var(--map-display-hover-attribution-text);
background-color: var(--map-attribution-bg-color);
}
} }
.bg-map-image { .bg-map-image {
@ -59,6 +65,7 @@
opacity: 0.6; opacity: 0.6;
height: 200px; height: 200px;
width: 100%; width: 100%;
filter: var(--map-filter);
} }
.map-attribution { .map-attribution {
@ -69,7 +76,8 @@
} }
.map-attribution-text { .map-attribution-text {
background-color: rgba(255, 255, 255, 0.7); color: var(--map-attribution-text);
background-color: var(--map-attribution-bg-color);
} }
} }
</style> </style>

View File

@ -6,13 +6,16 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ChartOptions, LayoutItem } from 'chart.js' import type { ChartOptions, LayoutItem } from 'chart.js'
import { computed, toRefs } from 'vue' import { computed, type ComputedRef, toRefs } from 'vue'
import { Bar } from 'vue-chartjs' import { Bar } from 'vue-chartjs'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
import { ROOT_STORE } from '@/store/constants'
import type { IChartDataset } from '@/types/chart' import type { IChartDataset } from '@/types/chart'
import type { TStatisticsDatasetKeys } from '@/types/statistics' import type { TStatisticsDatasetKeys } from '@/types/statistics'
import { formatTooltipValue } from '@/utils/tooltip' import { formatTooltipValue } from '@/utils/tooltip'
import { chartsColors } from '@/utils/workouts'
interface Props { interface Props {
datasets: IChartDataset[] datasets: IChartDataset[]
@ -32,8 +35,23 @@
useImperialUnits, useImperialUnits,
} = toRefs(props) } = toRefs(props)
const store = useStore()
const { t } = useI18n() const { t } = useI18n()
const darkMode: ComputedRef<boolean | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.DARK_MODE]
)
const lineColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.line
: chartsColors.ligthMode.line,
}))
const textColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.text
: chartsColors.ligthMode.text,
}))
const chartData = computed(() => ({ const chartData = computed(() => ({
labels: labels.value, labels: labels.value,
// workaround to avoid dataset modification // workaround to avoid dataset modification
@ -53,12 +71,23 @@
stacked: true, stacked: true,
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
},
ticks: {
...textColors.value,
}, },
}, },
y: { y: {
stacked: displayedData.value !== 'average_speed', stacked: displayedData.value !== 'average_speed',
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
}, },
ticks: { ticks: {
maxTicksLimit: 6, maxTicksLimit: 6,
@ -71,6 +100,7 @@
getUnit(displayedData.value) getUnit(displayedData.value)
) )
}, },
...textColors.value,
}, },
afterFit: function (scale: LayoutItem) { afterFit: function (scale: LayoutItem) {
scale.width = fullStats.value ? 90 : 60 scale.width = fullStats.value ? 90 : 60

View File

@ -34,7 +34,7 @@
padding: $default-padding * 0.5; padding: $default-padding * 0.5;
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
color: var(--app-color-light); color: var(--calendar-day-color);
} }
} }
</style> </style>

View File

@ -88,11 +88,11 @@
padding-left: 40px; padding-left: 40px;
.more-workouts { .more-workouts {
background: whitesmoke; background: var(--calendar-workouts-color);
border-radius: 4px; border-radius: 4px;
box-shadow: box-shadow:
0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 4px 8px 0 var(--calendar-workouts-box-shadow-0),
0 6px 20px 0 rgba(0, 0, 0, 0.19); 0 6px 20px 0 var(--calendar-workouts-box-shadow-1);
position: absolute; position: absolute;
top: 52px; top: 52px;
left: 0; left: 0;

View File

@ -60,12 +60,12 @@
{{ authUser.username }} {{ authUser.username }}
</router-link> </router-link>
<button <button
class="logout-button transparent" class="nav-button logout-button transparent"
@click="updateDisplayModal(true)" @click="updateDisplayModal(true)"
:aria-label="$t('user.LOGOUT')" :title="$t('user.LOGOUT')"
> >
<i class="fa fa-sign-out logout-fa" aria-hidden="true" /> <i class="fa fa-sign-out nav-button-fa" aria-hidden="true" />
<span class="logout-text">{{ $t('user.LOGOUT') }}</span> <span class="nav-button-text">{{ $t('user.LOGOUT') }}</span>
</button> </button>
</div> </div>
<div class="nav-items-group" v-else> <div class="nav-items-group" v-else>
@ -76,6 +76,27 @@
{{ $t('user.REGISTER') }} {{ $t('user.REGISTER') }}
</router-link> </router-link>
</div> </div>
<div class="theme-button">
<button
class="nav-button transparent"
@click="toggleTheme"
:title="$t('user.TOGGLE_THEME')"
>
<i
v-if="darkTheme"
class="fa nav-button-fa fa-moon"
aria-hidden="true"
/>
<img
v-else
class="clear-theme"
src="/img/weather/clear-day.svg"
alt=""
aria-hidden="true"
/>
<span class="nav-button-text">{{ $t('user.TOGGLE_THEME') }}</span>
</button>
</div>
<Dropdown <Dropdown
v-if="availableLanguages && language" v-if="availableLanguages && language"
class="nav-item" class="nav-item"
@ -93,7 +114,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, capitalize } from 'vue' import { computed, ref, capitalize, onBeforeMount, watch } from 'vue'
import type { ComputedRef, Ref } from 'vue' import type { ComputedRef, Ref } from 'vue'
import UserPicture from '@/components/User/UserPicture.vue' import UserPicture from '@/components/User/UserPicture.vue'
@ -119,6 +140,12 @@
) )
const isMenuOpen: Ref<boolean> = ref(false) const isMenuOpen: Ref<boolean> = ref(false)
const displayModal: Ref<boolean> = ref(false) const displayModal: Ref<boolean> = ref(false)
const darkMode: ComputedRef<boolean | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.DARK_MODE]
)
const darkTheme: ComputedRef<boolean> = computed(() => getDarkTheme())
onBeforeMount(() => setTheme())
function openMenu() { function openMenu() {
isMenuOpen.value = true isMenuOpen.value = true
@ -141,6 +168,32 @@
function updateDisplayModal(display: boolean) { function updateDisplayModal(display: boolean) {
displayModal.value = display displayModal.value = display
} }
function getDarkTheme() {
if (
darkMode.value === null &&
window.matchMedia('(prefers-color-scheme: dark)').matches
) {
return true
}
return darkMode.value === true
}
function setTheme() {
if (darkTheme.value) {
document.body.setAttribute('data-theme', 'dark')
} else {
document.body.removeAttribute('data-theme')
}
}
function toggleTheme() {
store.commit(ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE, !darkTheme.value)
}
watch(
() => darkTheme.value,
() => {
setTheme()
}
)
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
@ -252,16 +305,22 @@
.nav-separator { .nav-separator {
display: none; display: none;
} }
.logout-button { .nav-button {
padding: $default-padding * 0.5 $default-padding * 0.75; padding: $default-padding * 0.5 $default-padding * 0.75;
margin-left: 2px; margin-left: 2px;
.logout-fa { .nav-button-fa {
display: block; display: block;
} }
.logout-text { .nav-button-text {
display: none; display: none;
} }
} }
.clear-theme {
filter: var(--workout-img-color);
height: 20px;
margin-bottom: -5px;
}
} }
@media screen and (max-width: $medium-limit) { @media screen and (max-width: $medium-limit) {
@ -323,15 +382,16 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.logout-button { .nav-button {
padding: $default-padding $default-padding $default-padding padding: $default-padding $default-padding $default-padding
$default-padding * 2.4; $default-padding * 2.4;
color: var(--app-a-color); color: var(--app-a-color);
text-align: left; text-align: left;
.logout-fa { .nav-button-fa {
display: none; display: none;
width: 36px;
} }
.logout-text { .nav-button-text {
display: block; display: block;
} }
} }
@ -361,6 +421,9 @@
padding: 0; padding: 0;
} }
} }
.theme-button {
margin-left: $default-padding * 2;
}
} }
} }
</style> </style>

View File

@ -4,6 +4,8 @@
<dl> <dl>
<dt>{{ $t('user.PROFILE.LANGUAGE') }}:</dt> <dt>{{ $t('user.PROFILE.LANGUAGE') }}:</dt>
<dd>{{ userLanguage }}</dd> <dd>{{ userLanguage }}</dd>
<dt>{{ $t('user.PROFILE.THEME_MODE.LABEL') }}:</dt>
<dd>{{ $t(`user.PROFILE.THEME_MODE.VALUES.${darkMode}`) }}</dd>
<dt>{{ $t('user.PROFILE.TIMEZONE') }}:</dt> <dt>{{ $t('user.PROFILE.TIMEZONE') }}:</dt>
<dd>{{ timezone }}</dd> <dd>{{ timezone }}</dd>
<dt>{{ $t('user.PROFILE.DATE_FORMAT') }}:</dt> <dt>{{ $t('user.PROFILE.DATE_FORMAT') }}:</dt>
@ -95,6 +97,13 @@
const display_ascent = computed(() => const display_ascent = computed(() =>
props.user.display_ascent ? 'DISPLAYED' : 'HIDDEN' props.user.display_ascent ? 'DISPLAYED' : 'HIDDEN'
) )
const darkMode = computed(() =>
props.user.use_dark_mode === true
? 'DARK'
: props.user.use_dark_mode === false
? 'LIGHT'
: 'DEFAULT'
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -18,6 +18,22 @@
</option> </option>
</select> </select>
</label> </label>
<label class="form-items">
{{ $t('user.PROFILE.THEME_MODE.LABEL') }}
<select
id="use_dark_mode"
v-model="userForm.use_dark_mode"
:disabled="loading"
>
<option
v-for="mode in useDarkMode"
:value="mode.value"
:key="mode.label"
>
{{ $t(`user.PROFILE.THEME_MODE.VALUES.${mode.label}`) }}
</option>
</select>
</label>
<label class="form-items"> <label class="form-items">
{{ $t('user.PROFILE.TIMEZONE') }} {{ $t('user.PROFILE.TIMEZONE') }}
<TimezoneDropdown <TimezoneDropdown
@ -195,6 +211,7 @@
weekm: false, weekm: false,
start_elevation_at_zero: false, start_elevation_at_zero: false,
use_raw_gpx_speed: false, use_raw_gpx_speed: false,
use_dark_mode: false,
}) })
const weekStart = [ const weekStart = [
{ {
@ -246,6 +263,20 @@
value: true, value: true,
}, },
] ]
const useDarkMode = [
{
label: 'DARK',
value: true,
},
{
label: 'DEFAULT',
value: null,
},
{
label: 'LIGHT',
value: false,
},
]
const loading = computed( const loading = computed(
() => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING] () => store.getters[AUTH_USER_STORE.GETTERS.USER_LOADING]
) )
@ -279,6 +310,7 @@
userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris' userForm.timezone = user.timezone ? user.timezone : 'Europe/Paris'
userForm.date_format = user.date_format ? user.date_format : 'dd/MM/yyyy' userForm.date_format = user.date_format ? user.date_format : 'dd/MM/yyyy'
userForm.weekm = user.weekm ? user.weekm : false userForm.weekm = user.weekm ? user.weekm : false
userForm.use_dark_mode = user.use_dark_mode
} }
function updateProfile() { function updateProfile() {
store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES, userForm) store.dispatch(AUTH_USER_STORE.ACTIONS.UPDATE_USER_PREFERENCES, userForm)
@ -339,7 +371,8 @@
} }
#language, #language,
#date_format { #date_format,
#use_dark_mode {
padding: $default-padding * 0.5; padding: $default-padding * 0.5;
} }
} }

View File

@ -92,7 +92,7 @@
.policy-content { .policy-content {
height: 500px; height: 500px;
border: 1px solid #ccc; border: 1px solid var(--policy-border-color);
overflow: auto; overflow: auto;
margin: $default-margin; margin: $default-margin;
border-radius: $border-radius; border-radius: $border-radius;

View File

@ -238,6 +238,7 @@
height: 150px; height: 150px;
.no-map { .no-map {
line-height: 150px; line-height: 150px;
filter: var(--no-map-filter);
} }
::v-deep(.bg-map-image) { ::v-deep(.bg-map-image) {
height: 150px; height: 150px;

View File

@ -59,8 +59,10 @@
import type { ComputedRef } from 'vue' import type { ComputedRef } from 'vue'
import { Line } from 'vue-chartjs' import { Line } from 'vue-chartjs'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { useStore } from 'vuex'
import { htmlLegendPlugin } from '@/components/Workout/WorkoutDetail/WorkoutChart/legend' import { htmlLegendPlugin } from '@/components/Workout/WorkoutDetail/WorkoutChart/legend'
import { ROOT_STORE } from '@/store/constants'
import type { TUnit } from '@/types/units' import type { TUnit } from '@/types/units'
import type { IAuthUserProfile } from '@/types/user' import type { IAuthUserProfile } from '@/types/user'
import type { import type {
@ -69,7 +71,7 @@
TCoordinates, TCoordinates,
} from '@/types/workouts' } from '@/types/workouts'
import { units } from '@/utils/units' import { units } from '@/utils/units'
import { getDatasets } from '@/utils/workouts' import { chartsColors, getDatasets } from '@/utils/workouts'
interface Props { interface Props {
authUser: IAuthUserProfile authUser: IAuthUserProfile
@ -79,13 +81,22 @@
const emit = defineEmits(['getCoordinates']) const emit = defineEmits(['getCoordinates'])
const store = useStore()
const { t } = useI18n() const { t } = useI18n()
const { authUser, workoutData } = toRefs(props) const { authUser, workoutData } = toRefs(props)
const darkMode: ComputedRef<boolean | null> = computed(
() => store.getters[ROOT_STORE.GETTERS.DARK_MODE]
)
const displayDistance = ref(true) const displayDistance = ref(true)
const beginElevationAtZero = ref(authUser.value.start_elevation_at_zero) const beginElevationAtZero = ref(authUser.value.start_elevation_at_zero)
const datasets: ComputedRef<IWorkoutChartData> = computed(() => const datasets: ComputedRef<IWorkoutChartData> = computed(() =>
getDatasets(workoutData.value.chartData, t, authUser.value.imperial_units) getDatasets(
workoutData.value.chartData,
t,
authUser.value.imperial_units,
darkMode.value !== false
)
) )
const hasElevation = computed( const hasElevation = computed(
() => datasets.value && datasets.value.datasets.elevation.data.length > 0 () => datasets.value && datasets.value.datasets.elevation.data.length > 0
@ -106,6 +117,17 @@
const coordinates: ComputedRef<TCoordinates[]> = computed( const coordinates: ComputedRef<TCoordinates[]> = computed(
() => datasets.value.coordinates () => datasets.value.coordinates
) )
const lineColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.line
: chartsColors.ligthMode.line,
}))
const textColors = computed(() => ({
color: darkMode.value
? chartsColors.darkMode.text
: chartsColors.ligthMode.text,
}))
const options = computed<ChartOptions<'line'>>(() => ({ const options = computed<ChartOptions<'line'>>(() => ({
responsive: true, responsive: true,
maintainAspectRatio: false, maintainAspectRatio: false,
@ -119,6 +141,10 @@
x: { x: {
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
}, },
ticks: { ticks: {
count: 10, count: 10,
@ -127,6 +153,7 @@
? Number(value).toFixed(2) ? Number(value).toFixed(2)
: formatDuration(value) : formatDuration(value)
}, },
...textColors.value,
}, },
type: 'linear', type: 'linear',
bounds: 'data', bounds: 'data',
@ -135,16 +162,25 @@
text: displayDistance.value text: displayDistance.value
? t('workouts.DISTANCE') + ` (${fromKmUnit})` ? t('workouts.DISTANCE') + ` (${fromKmUnit})`
: t('workouts.DURATION'), : t('workouts.DURATION'),
...textColors.value,
}, },
}, },
ySpeed: { ySpeed: {
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
}, },
position: 'left', position: 'left',
title: { title: {
display: true, display: true,
text: t('workouts.SPEED') + ` (${fromKmUnit}/h)`, text: t('workouts.SPEED') + ` (${fromKmUnit}/h)`,
...textColors.value,
},
ticks: {
...textColors.value,
}, },
}, },
yElevation: { yElevation: {
@ -152,11 +188,19 @@
display: hasElevation.value, display: hasElevation.value,
grid: { grid: {
drawOnChartArea: false, drawOnChartArea: false,
...lineColors.value,
},
border: {
...lineColors.value,
}, },
position: 'right', position: 'right',
title: { title: {
display: true, display: true,
text: t('workouts.ELEVATION') + ` (${fromMUnit})`, text: t('workouts.ELEVATION') + ` (${fromMUnit})`,
...textColors.value,
},
ticks: {
...textColors.value,
}, },
}, },
}, },

View File

@ -143,6 +143,9 @@
.mountains { .mountains {
padding-right: $default-padding * 0.5; padding-right: $default-padding * 0.5;
} }
.mountains {
filter: var(--mountains-filter);
}
.workout-data { .workout-data {
padding: $default-padding * 0.5 0; padding: $default-padding * 0.5 0;

View File

@ -17,6 +17,7 @@
ref="workoutMap" ref="workoutMap"
@ready="fitBounds(bounds)" @ready="fitBounds(bounds)"
:use-global-leaflet="false" :use-global-leaflet="false"
class="map"
> >
<LControlLayers /> <LControlLayers />
<LControl <LControl
@ -212,13 +213,23 @@
} }
.no-map { .no-map {
line-height: 400px; line-height: 400px;
filter: var(--no-map-filter);
}
.leaflet-container {
.map {
filter: var(--map-filter);
} }
.map-control { .map-control {
background: #ffffff; background: var(--map-control-bg-color);
padding: 5px 10px; padding: 5px 10px;
border: 2px solid #bfc0ab; border: 2px solid var(--map-control-border-color);
border-radius: 3px; border-radius: 3px;
color: #000000; color: var(--map-control-color);
&:hover {
background-color: var(--dropdown-hover-color);
}
}
} }
::v-deep(.fullscreen) { ::v-deep(.fullscreen) {
display: flex; display: flex;

View File

@ -354,7 +354,7 @@
} }
.static-map { .static-map {
display: none; display: none;
box-shadow: 3px 3px 3px 1px lightgrey; box-shadow: 3px 3px 3px 1px var(--workout-static-map-shadow-color);
} }
} }
.workout-title:hover .static-map { .workout-title:hover .static-map {

View File

@ -115,6 +115,14 @@
"PROFILE": "profile", "PROFILE": "profile",
"SPORTS": "sports" "SPORTS": "sports"
}, },
"THEME_MODE": {
"LABEL": "Theme mode",
"VALUES": {
"DARK": "Dark",
"DEFAULT": "Browser preference",
"LIGHT": "Light"
}
},
"TIMEZONE": "Timezone", "TIMEZONE": "Timezone",
"UNITS": { "UNITS": {
"IMPERIAL": "Imperial system (ft, mi, mph, °F)", "IMPERIAL": "Imperial system (ft, mi, mph, °F)",
@ -136,6 +144,7 @@
"REVIEW": "review", "REVIEW": "review",
"SHOW_PASSWORD": "show password", "SHOW_PASSWORD": "show password",
"THIS_USER_ACCOUNT_IS_INACTIVE": "This user account is inactive.", "THIS_USER_ACCOUNT_IS_INACTIVE": "This user account is inactive.",
"TOGGLE_THEME": "Toggle theme (Light or Dark mode)",
"USERNAME": "Username", "USERNAME": "Username",
"USERNAME_INFO": "3 to 30 characters required, only alphanumeric characters and the underscore character \"_\" allowed.", "USERNAME_INFO": "3 to 30 characters required, only alphanumeric characters and the underscore character \"_\" allowed.",
"USER_PICTURE": "user picture", "USER_PICTURE": "user picture",

View File

@ -115,6 +115,14 @@
"PROFILE": "profil", "PROFILE": "profil",
"SPORTS": "sports" "SPORTS": "sports"
}, },
"THEME_MODE": {
"LABEL": "Thème",
"VALUES": {
"DARK": "Sombre",
"DEFAULT": "Préférence du navigateur",
"LIGHT": "Clair"
}
},
"TIMEZONE": "Fuseau horaire", "TIMEZONE": "Fuseau horaire",
"UNITS": { "UNITS": {
"IMPERIAL": "Système impérial (ft, mi, mph, °F)", "IMPERIAL": "Système impérial (ft, mi, mph, °F)",
@ -136,6 +144,7 @@
"REVIEW": "accepter", "REVIEW": "accepter",
"SHOW_PASSWORD": "afficher le mot de passe", "SHOW_PASSWORD": "afficher le mot de passe",
"THIS_USER_ACCOUNT_IS_INACTIVE": "Le compte de cet utilisateur est inactif.", "THIS_USER_ACCOUNT_IS_INACTIVE": "Le compte de cet utilisateur est inactif.",
"TOGGLE_THEME": "Modifier le thème (Mode Clair ou Sombre)",
"USERNAME": "Nom d'utilisateur", "USERNAME": "Nom d'utilisateur",
"USERNAME_INFO": "3 à 30 caractères requis, seuls les caractères alphanumériques et le caractère _ sont autorisés.", "USERNAME_INFO": "3 à 30 caractères requis, seuls les caractères alphanumériques et le caractère _ sont autorisés.",
"USER_PICTURE": "photo de l'utilisateur", "USER_PICTURE": "photo de l'utilisateur",

View File

@ -1,7 +1,12 @@
@import 'colors'; @import 'colors';
@import 'colors-dark';
@import 'fonts'; @import 'fonts';
@import 'vars'; @import 'vars';
html [data-theme='dark'] {
color-scheme: dark;
}
body { body {
margin: 0; margin: 0;
overflow-y: scroll; overflow-y: scroll;
@ -55,6 +60,7 @@ select {
background-color: var(--input-bg-color); background-color: var(--input-bg-color);
border-radius: $border-radius; border-radius: $border-radius;
border: solid 1px var(--input-border-color); border: solid 1px var(--input-border-color);
color: var(--input-color);
padding: $default-padding; padding: $default-padding;
&:disabled { &:disabled {
@ -102,13 +108,13 @@ button {
&:disabled, &:disabled,
&.confirm:disabled { &.confirm:disabled {
border-color: transparent; border-color: var(--disabled-border-color);
color: var(--disabled-color); color: var(--disabled-color);
} }
} }
&:hover { &:hover {
background: var(--app-color); background: var(--button-transparent-hover-color);
color: var(--button-hover-color); color: var(--button-hover-color);
} }
@ -128,7 +134,7 @@ button {
background: var(--button-cancel-bg-color); background: var(--button-cancel-bg-color);
color: var(--button-cancel-color); color: var(--button-cancel-color);
&:hover { &:hover {
background: var(--app-color); background: var(--button-transparent-hover-color);
color: var(--button-hover-color); color: var(--button-hover-color);
} }
} }
@ -137,7 +143,7 @@ button {
background: var(--button-confirm-bg-color); background: var(--button-confirm-bg-color);
color: var(--button-confirm-color); color: var(--button-confirm-color);
&:hover { &:hover {
background: var(--app-color); background: var(--button-transparent-hover-color);
color: var(--button-hover-color); color: var(--button-hover-color);
} }
} }
@ -194,7 +200,7 @@ button {
} }
.form-info { .form-info {
color: var(--alert-color); color: var(--form-info);
font-size: 0.8em; font-size: 0.8em;
margin-top: -0.2 * $default-margin; margin-top: -0.2 * $default-margin;
padding: 0 $default-padding * 1.5; padding: 0 $default-padding * 1.5;

View File

@ -0,0 +1,113 @@
:root [data-theme='dark'] {
--dark-blue: #181a1b;
--light-grey: #cfd0d0;
--app-background-color: var(--dark-blue);
--app-color: var(--light-grey);
--app-color-light: #6f7070;
--app-a-color: var(--light-grey);
--app-shadow-color: #383d3f;
--app-loading-color: #f3f3f3;
--app-loading-top-color: var(--app-color);
--button-hover-color: var(--app-color);
--button-transparent-hover-color: #233240;
--button-cancel-bg-color: var(--dark-blue);
--button-cancel-color: var(--app-color);
--button-confirm-bg-color: var(--dark-blue);
--button-confirm-color: var(--app-color);
--button-danger-bg-color: var(--dark-blue);
--button-danger-color: #dc3545;
--button-danger-hover-bg-color: #dc3545;
--button-danger-hover-color: var(--dark-blue);
--card-border-color: #494f52;
--input-border-color: #494f52;
--input-bg-color: var(--dark-blue);
--input-color: var(--app-color);
--input-error-color: #dc3545;
--dropdown-hover-color: #233240;
--dropdown-background-color: var(--dark-blue);
--dropdown-border-color: var(--input-border-color);
--policy-border-color: #ccc;
--box-shadow-color: lightgrey;
--admin-disabled-input-color: var(--dark-blue);
--custom-checkbox-border-color: #665f54;
--custom-checkbox-checked-bg-color: #575e62;
--custom-checkbox-checked-color: #e8e6e3;
--calendar-border-color: var(--input-border-color);
--calendar-week-end-color: #1e2021;
--calendar-day-color: var(--app-color);
--calendar-today-color: #202324;
--calendar-workouts-color: #233240;
--calendar-workouts-box-shadow-0: rgba(0, 0, 0, 0.2);
--calendar-workouts-box-shadow-1: rgba(0, 0, 0, 0.19);
--modal-background-color: rgba(0, 0, 0, 0.3);
--nav-bar-background-color: var(--dark-blue);
--nav-bar-link-active: #ffffff;
--nav-border-color: var(--input-border-color);
--mobile-menu-selected-color: var(--dark-blue);
--mobile-menu-selected-bgcolor: #9da3af;
--footer-background-color: var(--dark-blue);
--footer-border-color: var(--input-border-color);
--footer-color: #9f968a;
--form-info: var(--app-color);
--alert-background-color: #d6dde3;
--alert-color: #3f3f3f;
--info-background-color: #33353a;
--info-color: var(--app-color);
--error-background-color: #4e0000;
--error-color: #ea464f;
--success-background-color: #24391c;
--success-color: #97cd97;
--disabled-background-color: var(--dark-blue);
--disabled-border-color: transparent;
--disabled-color: #727272;
--disabled-sport-color: #616161;
--scroll-button-bg-color: var(--dark-blue);
--workout-trophy-color: #daa520;
--workout-img-color: invert(22%) sepia(25%) saturate(646%) hue-rotate(169deg)
brightness(97%) contrast(96%);
--workout-no-map-bg-color: #eaeaea;
--workout-no-map-color: #585959;
--map-control-color: #000000;
--map-control-bg-color: #ffffff;
--map-control-border-color: #bfc0ab;
--map-attribution-text: #e8e8e8;
--map-display-hover-attribution-text: #444444;
--map-attribution-bg-color: none;
--map-filter: invert(1) hue-rotate(180deg) brightness(0.8) contrast(0.8);
--map-display-hover-filter: invert(1) hue-rotate(180deg) brightness(1.5)
contrast(0.6);
--map-layers-overlays: var(--app-color);
--map-control-bar: var(--app-color);
--no-map-filter: invert(1) brightness(1.5) contrast(0.9);
--workout-static-map-shadow-color: #d2d2d2;
--mountains-filter: invert(90%) sepia(19%) saturate(0%) hue-rotate(39deg)
brightness(86%) contrast(102%);
--cell-heading-bg-color: #383838;
--cell-heading-color: #eeeeee;
--svg-filter: drop-shadow(10px 10px 10px var(--app-shadow-color));
--password-bg-color: #d7dadf;
--password-color-weak: #831819;
--password-color-medium: #9e6906;
--password-color-good: #4b5826;
--password-color-strong: #4a8c32;
--scroll-thumb-color: #949697;
}

View File

@ -8,6 +8,7 @@
--app-loading-top-color: var(--app-color); --app-loading-top-color: var(--app-color);
--button-hover-color: #ffffff; --button-hover-color: #ffffff;
--button-transparent-hover-color: var(--app-color);
--button-cancel-bg-color: #ffffff; --button-cancel-bg-color: #ffffff;
--button-cancel-color: var(--app-color); --button-cancel-color: var(--app-color);
--button-confirm-bg-color: #ffffff; --button-confirm-bg-color: #ffffff;
@ -20,8 +21,14 @@
--card-border-color: #c4c7cf; --card-border-color: #c4c7cf;
--input-border-color: #9da3af; --input-border-color: #9da3af;
--input-bg-color: #ffffff; --input-bg-color: #ffffff;
--input-color: var(--app-color);
--input-error-color: #dc3545; --input-error-color: #dc3545;
--dropdown-hover-color: #eff0f5; --dropdown-hover-color: #eff0f5;
--dropdown-background-color: #ffffff;
--dropdown-border-color: lightgrey;
--policy-border-color: #ccc;
--box-shadow-color: lightgrey;
--admin-disabled-input-color: #ffffff;
--custom-checkbox-border-color: #6d797a; --custom-checkbox-border-color: #6d797a;
--custom-checkbox-checked-bg-color: #6d797a; --custom-checkbox-checked-bg-color: #6d797a;
@ -29,7 +36,11 @@
--calendar-border-color: #c4c7cf; --calendar-border-color: #c4c7cf;
--calendar-week-end-color: #f5f5f5; --calendar-week-end-color: #f5f5f5;
--calendar-day-color: var(--app-color-light);
--calendar-today-color: #eff1f3; --calendar-today-color: #eff1f3;
--calendar-workouts-color: whitesmoke;
--calendar-workouts-box-shadow-0: rgba(0, 0, 0, 0.2);
--calendar-workouts-box-shadow-1: rgba(0, 0, 0, 0.19);
--modal-background-color: rgba(0, 0, 0, 0.3); --modal-background-color: rgba(0, 0, 0, 0.3);
@ -44,6 +55,8 @@
--footer-border-color: #ebeef3; --footer-border-color: #ebeef3;
--footer-color: #6f7070; --footer-color: #6f7070;
--form-info: var(--alert-color);
--alert-background-color: #d6dde3; --alert-background-color: #d6dde3;
--alert-color: #3f3f3f; --alert-color: #3f3f3f;
--info-background-color: #e5e7ea; --info-background-color: #e5e7ea;
@ -54,6 +67,7 @@
--success-color: #306430; --success-color: #306430;
--disabled-background-color: #e0e0e0; --disabled-background-color: #e0e0e0;
--disabled-border-color: transparent;
--disabled-color: #727272; --disabled-color: #727272;
--disabled-sport-color: #616161; --disabled-sport-color: #616161;
@ -64,6 +78,21 @@
brightness(97%) contrast(96%); brightness(97%) contrast(96%);
--workout-no-map-bg-color: #eaeaea; --workout-no-map-bg-color: #eaeaea;
--workout-no-map-color: #585959; --workout-no-map-color: #585959;
--map-control-color: #000000;
--map-control-bg-color: #ffffff;
--map-control-border-color: #bfc0ab;
--map-attribution-text: var(--app-color);
--map-display-hover-attribution-text: initial;
--map-attribution-bg-color: rgba(255, 255, 255, 0.7);
--map-filter: initial;
--map-display-hover-filter: initial;
--map-layers-overlays: initial;
--map-control-bar: #bfc0ab;
--no-map-filter: initial;
--workout-static-map-shadow-color: var(--app-shadow-color);
--mountains-filter: invert(19%) sepia(9%) saturate(2921%) hue-rotate(169deg)
brightness(85%) contrast(80%);
--cell-heading-bg-color: #eeeeee; --cell-heading-bg-color: #eeeeee;
--cell-heading-color: #696969; --cell-heading-color: #696969;

View File

@ -139,6 +139,10 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
res.data.data.language res.data.data.language
) )
} }
context.commit(
ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE,
res.data.data.use_dark_mode
)
context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS) context.dispatch(SPORTS_STORE.ACTIONS.GET_SPORTS)
} else { } else {
handleError(context, null) handleError(context, null)
@ -270,6 +274,10 @@ export const actions: ActionTree<IAuthUserState, IRootState> &
AUTH_USER_STORE.MUTATIONS.UPDATE_AUTH_USER_PROFILE, AUTH_USER_STORE.MUTATIONS.UPDATE_AUTH_USER_PROFILE,
res.data.data res.data.data
) )
context.commit(
ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE,
res.data.data.use_dark_mode
)
context context
.dispatch( .dispatch(
ROOT_STORE.ACTIONS.UPDATE_APPLICATION_LANGUAGE, ROOT_STORE.ACTIONS.UPDATE_APPLICATION_LANGUAGE,

View File

@ -10,6 +10,7 @@ export enum RootGetters {
APP_CONFIG = 'APP_CONFIG', APP_CONFIG = 'APP_CONFIG',
APP_LOADING = 'APP_LOADING', APP_LOADING = 'APP_LOADING',
APP_STATS = 'APP_STATS', APP_STATS = 'APP_STATS',
DARK_MODE = 'DARK_MODE',
ERROR_MESSAGES = 'ERROR_MESSAGES', ERROR_MESSAGES = 'ERROR_MESSAGES',
LANGUAGE = 'LANGUAGE', LANGUAGE = 'LANGUAGE',
LOCALE = 'LOCALE', // date-fns LOCALE = 'LOCALE', // date-fns
@ -22,5 +23,6 @@ export enum RootMutations {
UPDATE_APPLICATION_LOADING = 'UPDATE_APPLICATION_LOADING', UPDATE_APPLICATION_LOADING = 'UPDATE_APPLICATION_LOADING',
UPDATE_APPLICATION_PRIVACY_POLICY = 'UPDATE_APPLICATION_PRIVACY_POLICY', UPDATE_APPLICATION_PRIVACY_POLICY = 'UPDATE_APPLICATION_PRIVACY_POLICY',
UPDATE_APPLICATION_STATS = 'UPDATE_APPLICATION_STATS', UPDATE_APPLICATION_STATS = 'UPDATE_APPLICATION_STATS',
UPDATE_DARK_MODE = 'UPDATE_DARK_MODE',
UPDATE_LANG = 'UPDATE_LANG', UPDATE_LANG = 'UPDATE_LANG',
} }

View File

@ -13,6 +13,9 @@ export const getters: GetterTree<IRootState, IRootState> & IRootGetters = {
[ROOT_STORE.GETTERS.APP_STATS]: (state: IRootState) => { [ROOT_STORE.GETTERS.APP_STATS]: (state: IRootState) => {
return state.application.statistics return state.application.statistics
}, },
[ROOT_STORE.GETTERS.DARK_MODE]: (state: IRootState) => {
return state.darkMode
},
[ROOT_STORE.GETTERS.ERROR_MESSAGES]: (state: IRootState) => { [ROOT_STORE.GETTERS.ERROR_MESSAGES]: (state: IRootState) => {
return state.errorMessages return state.errorMessages
}, },

View File

@ -45,4 +45,10 @@ export const mutations: MutationTree<IRootState> & TRootMutations = {
state.language = language state.language = language
state.locale = localeFromLanguage[language] state.locale = localeFromLanguage[language]
}, },
[ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE](
state: IRootState,
darkMode: boolean | null
) {
state.darkMode = darkMode
},
} }

View File

@ -17,4 +17,5 @@ export const state: IRootState = {
}, },
}, },
appLoading: false, appLoading: false,
darkMode: null,
} }

View File

@ -22,6 +22,7 @@ export interface IRootState {
errorMessages: string | string[] | null errorMessages: string | string[] | null
application: IApplication application: IApplication
appLoading: boolean appLoading: boolean
darkMode: boolean | null
} }
export interface IRootActions { export interface IRootActions {
@ -51,6 +52,8 @@ export interface IRootGetters {
[ROOT_STORE.GETTERS.APP_STATS](state: IRootState): IAppStatistics [ROOT_STORE.GETTERS.APP_STATS](state: IRootState): IAppStatistics
[ROOT_STORE.GETTERS.DARK_MODE](state: IRootState): boolean | null
[ROOT_STORE.GETTERS.ERROR_MESSAGES]( [ROOT_STORE.GETTERS.ERROR_MESSAGES](
state: IRootState state: IRootState
): string | string[] | null ): string | string[] | null
@ -83,6 +86,10 @@ export type TRootMutations<S = IRootState> = {
statistics: IAppStatistics statistics: IAppStatistics
): void ): void
[ROOT_STORE.MUTATIONS.UPDATE_LANG](state: S, language: TLanguage): void [ROOT_STORE.MUTATIONS.UPDATE_LANG](state: S, language: TLanguage): void
[ROOT_STORE.MUTATIONS.UPDATE_DARK_MODE](
state: S,
darkMode: boolean | null
): void
} }
export type TRootStoreModule<S = IRootState> = Omit< export type TRootStoreModule<S = IRootState> = Omit<

View File

@ -35,6 +35,7 @@ export interface IAuthUserProfile extends IUserProfile {
timezone: string timezone: string
date_format: string date_format: string
weekm: boolean weekm: boolean
use_dark_mode: boolean | null
} }
export interface IUserPayload { export interface IUserPayload {
@ -73,6 +74,7 @@ export interface IUserPreferencesPayload {
timezone: string timezone: string
date_format: string date_format: string
weekm: boolean weekm: boolean
use_dark_mode: boolean | null
} }
export interface IUserSportPreferencesPayload { export interface IUserSportPreferencesPayload {

View File

@ -7,24 +7,37 @@ import type {
} from '@/types/workouts' } from '@/types/workouts'
import { convertStatsDistance } from '@/utils/units' import { convertStatsDistance } from '@/utils/units'
export const chartsColors = {
ligthMode: {
// default chartjs values
text: '#666',
line: 'rgba(0, 0, 0, 0.1)',
},
darkMode: {
text: '#a1a1a1',
line: '#3f3f3f',
},
}
export const getDatasets = ( export const getDatasets = (
chartData: IWorkoutApiChartData[], chartData: IWorkoutApiChartData[],
t: CallableFunction, t: CallableFunction,
useImperialUnits: boolean useImperialUnits: boolean,
useDarkMode: boolean = false
): IWorkoutChartData => { ): IWorkoutChartData => {
const datasets: TWorkoutDatasets = { const datasets: TWorkoutDatasets = {
speed: { speed: {
label: t('workouts.SPEED'), label: t('workouts.SPEED'),
backgroundColor: ['#FFFFFF'], backgroundColor: ['transparent'],
borderColor: ['#8884d8'], borderColor: [useDarkMode ? '#5f5c97' : '#8884d8'],
borderWidth: 2, borderWidth: 2,
data: [], data: [],
yAxisID: 'ySpeed', yAxisID: 'ySpeed',
}, },
elevation: { elevation: {
label: t('workouts.ELEVATION'), label: t('workouts.ELEVATION'),
backgroundColor: ['#e5e5e5'], backgroundColor: [useDarkMode ? '#303030' : '#e5e5e5'],
borderColor: ['#cccccc'], borderColor: [useDarkMode ? '#222222' : '#cccccc'],
borderWidth: 1, borderWidth: 1,
fill: true, fill: true,
data: [], data: [],

View File

@ -72,8 +72,8 @@
&:disabled { &:disabled {
-webkit-appearance: none; -webkit-appearance: none;
-moz-appearance: textfield; -moz-appearance: textfield;
background-color: white; background-color: var(--admin-disabled-input-color);
border-color: white; border-color: var(--admin-disabled-input-color);
color: var(--app-color); color: var(--app-color);
} }
} }

View File

@ -21,7 +21,7 @@ describe('getDatasets', () => {
datasets: { datasets: {
speed: { speed: {
label: 'vitesse', label: 'vitesse',
backgroundColor: ['#FFFFFF'], backgroundColor: ['transparent'],
borderColor: ['#8884d8'], borderColor: ['#8884d8'],
borderWidth: 2, borderWidth: 2,
data: [], data: [],
@ -81,7 +81,7 @@ describe('getDatasets', () => {
datasets: { datasets: {
speed: { speed: {
label: 'speed', label: 'speed',
backgroundColor: ['#FFFFFF'], backgroundColor: ['transparent'],
borderColor: ['#8884d8'], borderColor: ['#8884d8'],
borderWidth: 2, borderWidth: 2,
data: [2.89, 20.64, 13.03], data: [2.89, 20.64, 13.03],
@ -145,7 +145,7 @@ describe('getDatasets', () => {
datasets: { datasets: {
speed: { speed: {
label: 'speed', label: 'speed',
backgroundColor: ['#FFFFFF'], backgroundColor: ['transparent'],
borderColor: ['#8884d8'], borderColor: ['#8884d8'],
borderWidth: 2, borderWidth: 2,
data: [1.8, 12.83, 8.1], data: [1.8, 12.83, 8.1],
@ -183,6 +183,90 @@ describe('getDatasets', () => {
}) })
}) })
describe('getDatasets with dark mode', () => {
const testparams = [
{
description: 'it returns dark mode color',
inputParams: {
charData: [],
locale: 'fr',
useImperialUnits: false,
useDarkMode: true,
},
expected: {
distance_labels: [],
duration_labels: [],
datasets: {
speed: {
label: 'vitesse',
backgroundColor: ['transparent'],
borderColor: ['#5f5c97'],
borderWidth: 2,
data: [],
yAxisID: 'ySpeed',
},
elevation: {
label: 'altitude',
backgroundColor: ['#303030'],
borderColor: ['#222222'],
borderWidth: 1,
fill: true,
data: [],
yAxisID: 'yElevation',
},
},
coordinates: [],
},
},
{
description: 'it returns light mode color',
inputParams: {
charData: [],
locale: 'fr',
useImperialUnits: false,
useDarkMode: false,
},
expected: {
distance_labels: [],
duration_labels: [],
datasets: {
speed: {
label: 'vitesse',
backgroundColor: ['transparent'],
borderColor: ['#8884d8'],
borderWidth: 2,
data: [],
yAxisID: 'ySpeed',
},
elevation: {
label: 'altitude',
backgroundColor: ['#e5e5e5'],
borderColor: ['#cccccc'],
borderWidth: 1,
fill: true,
data: [],
yAxisID: 'yElevation',
},
},
coordinates: [],
},
},
]
testparams.map((testParams) => {
it(testParams.description, () => {
locale.value = testParams.inputParams.locale
expect(
getDatasets(
testParams.inputParams.charData,
t,
testParams.inputParams.useImperialUnits,
testParams.inputParams.useDarkMode
)
).toStrictEqual(testParams.expected)
})
})
})
describe('getDonutDatasets', () => { describe('getDonutDatasets', () => {
const testparams = [ const testparams = [
{ {