This commit is contained in:
2024-07-08 22:46:35 +02:00
parent 02f44c49d2
commit 27254d817a
56249 changed files with 808097 additions and 1 deletions

View File

@@ -0,0 +1,302 @@
/* extension.js
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
import GLib from 'gi://GLib'
import Clutter from 'gi://Clutter'
import St from 'gi://St'
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'
import * as Main from 'resource:///org/gnome/shell/ui/main.js'
const MainPanel = Main.panel
import {
getCurrentLocale,
getCurrentCalendar,
getCurrentTimezone,
updateLevel,
TEXT_ALIGN_CENTER,
} from './utils/general.js'
import { FormatterManager } from './utils/formatter.js'
import * as prefFields from './utils/prefFields.js'
let PATTERN = ''
let USE_DEFAULT_LOCALE = true
let USE_DEFAULT_CALENDAR = true
let USE_DEFAULT_TIMEZONE = true
let CUSTOM_LOCALE = ''
let CUSTOM_CALENDAR = ''
let CUSTOM_TIMEZONE = ''
let REMOVE_MESSAGES_INDICATOR = false
let APPLY_ALL_PANELS = false
let FONT_SIZE = 1
let EVERY = null
let TEXT_ALIGN_MODE = ''
function _getDateMenuButton(panel) {
return panel.statusArea.dateMenu.get_children()[0]
}
export default class DateMenuFormatter extends Extension {
constructor(metadata) {
super(metadata)
this.formatters = null
this._formatters_load_promise = null
this._displays = null
this._timerId = -1
this._settingsChangedId = null
this._dashToPanelConnection = null
this._formatter = null
this._update = true
}
_createDisplay() {
const display = new St.Label({
style_class: 'clock',
style: 'font-size: 9pt; text-align: center',
})
display.clutter_text.x_align = Clutter.ActorAlign.CENTER
display.clutter_text.y_align = Clutter.ActorAlign.CENTER
display.text = '...'
return display
}
_loadSettings() {
this._settings = this.getSettings()
this._settingsChangedId = this._settings.connect(
'changed',
this._onSettingsChange.bind(this)
)
this._onSettingsChange()
}
_fetchSettings() {
PATTERN = this._settings.get_string(prefFields.PATTERN)
REMOVE_MESSAGES_INDICATOR = this._settings.get_boolean(
prefFields.REMOVE_MESSAGES_INDICATOR
)
USE_DEFAULT_LOCALE = this._settings.get_boolean(
prefFields.USE_DEFAULT_LOCALE
)
CUSTOM_LOCALE = this._settings.get_string(prefFields.CUSTOM_LOCALE)
USE_DEFAULT_CALENDAR = this._settings.get_boolean(
prefFields.USE_DEFAULT_CALENDAR
)
CUSTOM_CALENDAR = this._settings.get_string(prefFields.CUSTOM_CALENDAR)
USE_DEFAULT_TIMEZONE = this._settings.get_boolean(
prefFields.USE_DEFAULT_TIMEZONE
)
CUSTOM_TIMEZONE = this._settings.get_string(prefFields.CUSTOM_TIMEZONE)
APPLY_ALL_PANELS = this._settings.get_boolean(prefFields.APPLY_ALL_PANELS)
FONT_SIZE = this._settings.get_int(prefFields.FONT_SIZE)
TEXT_ALIGN_MODE =
this._settings.get_string(prefFields.TEXT_ALIGN) || TEXT_ALIGN_CENTER
const curLvl = this._settings.get_int(prefFields.UPDATE_LEVEL)
if (EVERY.lvl !== curLvl) {
EVERY = updateLevel(curLvl)
if (this._timerId !== -1) this.restart()
}
const locale = USE_DEFAULT_LOCALE ? getCurrentLocale() : CUSTOM_LOCALE
const calendar = USE_DEFAULT_CALENDAR
? getCurrentCalendar()
: CUSTOM_CALENDAR
const timezone = USE_DEFAULT_TIMEZONE
? getCurrentTimezone()
: CUSTOM_TIMEZONE
this._formatters_load_promise.then(() => {
const formatterKey = this._settings.get_string(prefFields.FORMATTER)
const formatter = this.formatters.getFormatter(formatterKey)
if (formatter) {
this._formatter = new formatter(timezone, locale, calendar)
}
})
}
_removeIndicator(panels) {
panels.forEach((panel) => {
if (panel.statusArea.dateMenu._indicator.get_parent())
_getDateMenuButton(panel).remove_child(
panel.statusArea.dateMenu._indicator
)
})
}
_restoreIndicator(panels) {
panels.forEach((panel) => {
if (!panel.statusArea.dateMenu._indicator.get_parent())
_getDateMenuButton(panel).insert_child_at_index(
panel.statusArea.dateMenu._indicator,
2
)
})
}
// returns affected and unaffected panels based on settings and Dash To Panel availability
_getPanels() {
if (!global.dashToPanel) return [[MainPanel], []]
else if (APPLY_ALL_PANELS) {
return [global.dashToPanel.panels, []]
} else {
// MainPanel is not the same as primary Dash To Panel panel, but their dateMenus are the same
return [
[MainPanel],
global.dashToPanel.panels.filter(
(panel) => panel.statusArea.dateMenu != MainPanel.statusArea.dateMenu
),
]
}
}
_enableOn(panels) {
panels.forEach((panel, idx) => {
const dateMenuButton = _getDateMenuButton(panel)
if (!this._displays[idx].get_parent()) {
dateMenuButton.insert_child_at_index(this._displays[idx], 1)
dateMenuButton.dateMenuFormatterDisplay = this._displays[idx]
}
if (panel.statusArea.dateMenu._clockDisplay.get_parent()) {
dateMenuButton.remove_child(panel.statusArea.dateMenu._clockDisplay)
}
})
}
_disableOn(panels) {
panels.forEach((panel) => {
const dateMenuButton = _getDateMenuButton(panel)
if (!panel.statusArea.dateMenu._clockDisplay.get_parent()) {
dateMenuButton.insert_child_at_index(
panel.statusArea.dateMenu._clockDisplay,
1
)
}
if (
dateMenuButton.dateMenuFormatterDisplay &&
dateMenuButton.dateMenuFormatterDisplay.get_parent()
) {
dateMenuButton.remove_child(dateMenuButton.dateMenuFormatterDisplay)
}
})
}
_onSettingsChange() {
this._fetchSettings()
// does Dash to Panel support more than 2 panels? better to be safe than sorry
if (
global.dashToPanel &&
this._displays.length < global.dashToPanel.panels.length
) {
const missingPanels =
global.dashToPanel.panels.length - this._displays.length
this._displays = [
...this._displays,
...Array.from({ length: missingPanels }, () => this._createDisplay()),
]
}
const [affectedPanels, unaffectedPanels] = this._getPanels()
if (REMOVE_MESSAGES_INDICATOR) {
this._removeIndicator(affectedPanels)
this._restoreIndicator(unaffectedPanels)
} else {
this._restoreIndicator([...affectedPanels, ...unaffectedPanels])
}
this._enableOn(affectedPanels)
this._disableOn(unaffectedPanels)
this._displays.forEach(
(display) =>
(display.style = `font-size: ${FONT_SIZE}pt; text-align: ${TEXT_ALIGN_MODE}`)
)
}
enable() {
EVERY = updateLevel()
this.formatters = new FormatterManager()
this._formatters_load_promise = this.formatters.loadFormatters()
this._displays = [this._createDisplay()]
if (global.dashToPanel) {
this._dashToPanelConnection = global.dashToPanel.connect(
'panels-created',
() => this._onSettingsChange()
)
}
this._loadSettings()
const [affectedPanels, _] = this._getPanels()
this._enableOn(affectedPanels)
this.start()
}
start() {
this._update = true
this._timerId = GLib.timeout_add(EVERY.priority, EVERY.timeout, () =>
this.update()
)
this.update()
}
stop(force) {
if (force) {
GLib.Source.remove(this._timerId)
} else {
this._update = false
}
}
restart() {
this.stop(true)
this.start()
}
update() {
const setText = (text) =>
this._displays.forEach((display) => (display.text = text))
try {
setText(this._formatter.format(PATTERN, new Date()))
} catch (e) {
// if there is an exception during formatting, use the default display's text
setText(MainPanel.statusArea.dateMenu._clockDisplay.text)
if (this._formatter !== null && this._formatter !== undefined)
console.log('DateMenuFormatter: ' + e.message)
}
return this._update
}
disable() {
EVERY = null
const [affectedPanels, unaffectedPanels] = this._getPanels()
const allPanels = [...affectedPanels, ...unaffectedPanels]
this._disableOn(allPanels)
this._restoreIndicator(allPanels)
this.stop()
if (this._settingsChangedId) {
this._settings.disconnect(this._settingsChangedId)
this._settingsChangedId = null
}
if (this._dashToPanelConnection) {
global.dashToPanel.disconnect(this._dashToPanelConnection)
this._dashToPanelConnection = null
}
this._settings = null
this.formatters = null
this._formatter = null
this._formatters_load_promise = null
this.display?.forEach((d) => d?.destroy())
this._displays = null
}
}

View File

@@ -0,0 +1,86 @@
import { DateTime } from '../lib/luxon.js'
import { createFormatter, FormatterHelp } from '../utils/formatter.js'
export default class extends createFormatter('Luxon', '', {
customTimezone: true,
customLocale: true,
customCalendar: true,
}) {
config(timezone, locale, calendar) {
this._timezone = timezone
this._locale = locale
this._calendar = calendar
}
format(pattern, date) {
return DateTime.fromJSDate(date)
.setZone(this._timezone)
.reconfigure({
locale: this._locale,
outputCalendar: this._calendar,
})
.toFormat(pattern.replaceAll('\\n', '\n'))
}
}
export function help() {
return new FormatterHelp(
'https://moment.github.io/luxon/#/formatting?id=table-of-tokens',
[
['y', 'year', '2023'],
['yy', 'year (2 digits only)', '23'],
['', '', ''],
['M', 'month (numeric)', '4'],
['MM', 'month (numeric, padded)', '04'],
['MMM', 'month (short)', 'Apr'],
['MMMM', 'month (full)', 'April'],
['MMMMM', 'month (narrow)', 'A'],
['', '', ''],
['', '', ''],
['', '', ''],
['', '', ''],
['kk', 'ISO week year', '14'],
['kkkk', 'ISO week year (padded to 4)', '2014'],
['W', 'ISO week number', '32'],
['WW', 'ISO week number (padded to 2)', '32'],
['', '', ''],
['d', 'day of month', '7'],
['dd', 'day of month (padded)', '07'],
['o', 'day of year (unpadded)', '98'],
['ooo', 'day of year (padded to 3)', '002'],
['', '', ''],
['s', 'second (unpadded)', '4'],
['ss', 'second (padded to 2)', '04'],
['', '', ''],
["'text'", 'literal text', ''],
],
[
['E', 'weekday (numeric)', '1-7'],
['EEE', 'weekday (abbrev.)', 'Tue'],
['EEEE', 'weekday (full)', 'Tuesday'],
['EEEEE', 'weekday (narrow)', 'T'],
['', '', ''],
['h', 'hour', '1-12'],
['hh', 'hour (padded)', '01-12'],
['H', 'hour', '0-23'],
['HH', 'hour (padded)', '00-23'],
['a', 'AM-PM', ''],
['', '', ''],
['ii', 'Local week year', '14'],
['iiii', 'Local week year (padded to 4)', '2014'],
['n', 'Local week number', '32'],
['nn', 'Local week number (padded to 2)', '32'],
['', '', ''],
['m', 'minute', '7'],
['mm', 'minute (padded)', '07'],
['', '', ''],
['q', 'quarter', '3'],
['qq', 'quarter (padded)', '03'],
['', '', ''],
['S', 'millisecond (unpadded)', '4'],
['SSS', 'millisecond (padded to 3)', '004'],
['', '', ''],
['\\n', 'new line', ''],
]
)
}

View File

@@ -0,0 +1,68 @@
import { SimpleDateFormat } from '../lib/SimpleDateFormat.js'
import { createFormatter, FormatterHelp } from '../utils/formatter.js'
function convertToPattern(str) {
return '#' + str.replace(/\\n/g, '\n').replace(/''/g, '>`<')
}
function convertFromPattern(str) {
return str.replace(/>`</g, "'")
}
export default class extends createFormatter('SimpleDateFormat', '', {
customLocale: true,
}) {
config(_, locale) {
this._formatter = new SimpleDateFormat(locale)
}
format(pattern, date) {
return convertFromPattern(
this._formatter.format(convertToPattern(pattern), date)
)
}
}
export function help() {
return new FormatterHelp(
'https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table',
[
['y', 'year', '2023'],
['yy', 'year (2 digits only)', '23'],
['', '', ''],
['M', 'month (numeric)', '4'],
['MM', 'month (numeric, padded)', '04'],
['MMM', 'month (short)', 'Apr'],
['MMMM', 'month (full)', 'April'],
['MMMMM', 'month (narrow)', 'A'],
['', '', ''],
['w', 'week of year', '9'],
['ww', 'week of year (padded)', '09'],
['W', 'week of month', '2'],
['', '', ''],
['d', 'day of month', '7'],
['dd', 'day of month (padded)', '07'],
['', '', ''],
["'text'", 'literal text', ''],
],
[
['EEE', 'weekday (abbrev.)', 'Tue'],
['EEEE', 'weekday (full)', 'Tuesday'],
['EEEEE', 'weekday (narrow)', 'T'],
['EEEEEE', 'weekday (short)', 'Tu'],
['', '', ''],
['h', 'hour', '1-12'],
['hh', 'hour (padded)', '01-12'],
['k', 'hour', '0-23'],
['kk', 'hour (padded)', '00-23'],
['', '', ''],
['m', 'minute', '7'],
['mm', 'minute (padded)', '07'],
['', '', ''],
['aaa', 'period (am/pm)', ''],
['', '', ''],
['', '', ''],
['\\n', 'new line', ''],
]
)
}

View File

@@ -0,0 +1,27 @@
import { DateTime } from '../lib/luxon.js'
import { createFormatter, FormatterHelp } from '../utils/formatter.js'
export default class extends createFormatter('Swatch Beats') {
format(pattern, date) {
const dateTime = DateTime.fromJSDate(date)
.setZone('Europe/Zurich')
.setLocale('CH')
const timeInSeconds =
(dateTime.hour * 60 + dateTime.minute) * 60 + dateTime.second
// there are 86.4 seconds in a beat
const secondsInABeat = 86.4
// calculate beats to two decimal places
const [beats, subbeats] = Math.abs(timeInSeconds / secondsInABeat)
.toFixed(2)
.split('.')
return pattern.replaceAll('b', `@${beats}`).replaceAll('s', `.${subbeats}`)
}
}
export function help() {
return new FormatterHelp(
'',
[['b', 'Beats', '@500']],
[['s', 'Sub Beats', '.12']]
)
}

View File

@@ -0,0 +1,522 @@
/*
https://github.com/ray007/simple_dt.js
License: GNU Lesser General Public License v2.1
*/
/**
* http://userguide.icu-project.org/formatparse/datetime
* http://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table
*/
export const SimpleDateFormat = class {
/**
* @param {string} locale
* @param {boolean=} [utc=false]
*/
constructor(locale, utc) {
this.locale = locale;
this.utc = !!utc;
this.fmt1 = {}; // cache formatters for single ICU pattern part
this.fmtC = {}; // cache for more complete formatters
this.$dpFn = {}; // cache for Date prototype methods for use in formatters
this.prepDateFns();
}
/**
* get cache or new formatter creator
* @param {string} locale
* @param {boolean=} [utc=false] format dates/times for target-TZ UTC
*/
static get(locale, utc) {
var key = (locale || 'default');
if (utc) key += ':utc';
return formatters[key] || (formatters[key] = new SimpleDateFormat(locale, utc));
}
/**
* format given date to pattern
* @param {!string} pat
* @param {!Date} d
* @returns {string}
*/
format(pat, d) {
var fmtFn = this.getFormatter(pat);
return fmtFn(d);
}
/**
* get formatting function for given pattern
* @param {string} pat
* @returns {!dateFmtFn}
*/
getFormatter(pat) {
var fnCt = this.fmtC, f = fnCt[pat];
if (!f && !(pat in fnCt)) {
if (pat[0] == '#') {
f = this.mkPatternFormatter(pat.substr(1));
} else { // autoformat -> DateTimeFormat
f = this.mkFmtFnDtf(pat);
}
fnCt[pat] = f;
}
return f;
}
/**
* create instance of Intl.DateTimeFormat for given pattern
* @param {string} pat SDF pattern for formatting
* @return {Intl.DateTimeFormat}
*/
getDTF(pat) {
// prep options
var o = SimpleDateFormat.dtfOptions(pat);
// prep formatter to work with
if (this.utc) o['timeZone'] = 'UTC';
return new Intl.DateTimeFormat(this.getLocales(o), o);
}
/**
* prepare quick get/set function for date parts
* @private
*/
prepDateFns() {
var dpFn = this.$dpFn, utc = this.utc, dp = Date.prototype;
dateFnParts.forEach((part) => {
var fnPart = utc ? ('UTC' + part) : part;
dpFn['get' + part] = dp['get' + fnPart];
dpFn['set' + part] = dp['set' + fnPart];
});
dpFn.getTimezoneOffset = dp.getTimezoneOffset;
}
/**
* create formatting function for SDF pattern (not auto-format)
* @private
* @param {string} pat SDF pattern input
* @returns {?dateFmtFn}
*/
mkPatternFormatter(pat) {
var rx = /([a-zA-Z'])\1*/g, m, fmtParts = [], i0 = 0, fmtFn = null;
// eslint-disable-next-line no-cond-assign
while (m = rx.exec(pat)) {
// m: [pat1, sig]
var pat1 = m[0], sig = m[1], i = rx.lastIndex, l = pat1.length, i1 = i - l;
// "l" is deprecated
if (sig == 'l') continue;
// some formatters are autoformat only
if ('jJC'.indexOf(sig) >= 0) continue;
// let's get to work
if (i1 > i0)
fmtParts.push(pat.substring(i0, i1));
if (sig == "'") {
if (l > 1) fmtParts.push(pat1.substr(0, l >> 1));
if (l & 1) {
i1 = pat.indexOf("'", i);
if (i1 > 0) {
fmtParts.push(pat.substring(i, i1));
rx.lastIndex = i = i1 + 1;
}
}
} else { // a SimpleDateFormat pattern specifier
var p1f = this.getFmtSdf1(pat1);
if (p1f) fmtParts.push(p1f);
}
i0 = i;
}
// and return a formatting function for all of this
if (fmtParts.length) {
if (fmtParts.length > 1) {
fmtFn = (d) => {
var s = '';
fmtParts.forEach((x) => {
if (x instanceof Function)
x = x(d); // use sub-formatter
s += x;
});
return s;
};
} else { // length == 1
fmtFn = fmtParts[0];
}
}
return fmtFn;
}
/**
* get formatter for single SimpleDateFormat specifier
* @private
* @param {string} pat1
* @returns {dateFmtFn|string}
*/
getFmtSdf1(pat1) {
var fnCt = this.fmt1, fmtFn = fnCt[pat1];
if (!fmtFn && !(pat1 in fnCt)) {
fmtFn = fnCt[pat1] = this.getFmt1Fn(pat1);
}
return fmtFn;
}
/**
* get non-DTF formatter for single SDF pattern
* @private
* @param {string} pat1
* @returns {dateFmtFn}
*/
getFmt1Fn(pat1) {
return this.mkFmtFnDtf(pat1, 1) || this.getPatFn(pat1);
}
/**
* get pre-defined formatting function for pattern
* @private
* @param {string} pat1
* @returns {dateFmtFn}
*/
getPatFn(pat1) {
var fn, utc = this.utc, sig = pat1[0];
if (pat1.length > 5 && 'eEc'.indexOf(sig) >= 0) {
// weekday: 6-len e, E or c
var fShort = this.getFmt1Fn(pat1.substr(0,3));
if (fShort(new Date()).length > 2) {
fn = (d) => fShort(d).substr(0,2);
} else {
fn = fShort;
}
} else { // length 1..5 (as usual)
var fnSdf = sdfFnPat[pat1];
if (fnSdf) {
fn = (d) => fnSdf(d, pat1, utc);
} else {
fnSdf = sdfFnSig[sig];
fn = fnSdf && ((d) => fnSdf(d, pat1, utc));
}
}
return fn;
}
/**
* get project locale to use with DateTimeFormat (BCP47 tag)
* @private
* @param {Object=} o options - may influence locale
* @returns {string}
*/
// eslint-disable-next-line no-unused-vars
getLocales(o) {
return this.locale || (typeof navigator != 'undefined' ? navigator.language : 'en-US');
}
/**
* create formatting function using an instance of {@see Intl.DateTimeFormat} for given pattern
* @private
* @param {string} pat SDF pattern for autoformat
* @param {?=} [f1=false] truthy: single pattern to generate formatter for?
* @returns {?dateFmtFn}
*/
mkFmtFnDtf(pat, f1) {
// prep options
var o = {}, utc = this.utc, sig = pat[0];
if (f1) {
// len=6 only allowed for weekdays, do not use DTF for that
if (pat.length > 5 && sig != 'S' && sig != 'A') return null;
if (!SimpleDateFormat.sdf2dtfO(pat, o)) return null;
} else {
SimpleDateFormat.dtfOptions(pat, o);
}
// prep formatter to work with
if (utc) o['timeZone'] = 'UTC';
var fn, dtf = new Intl.DateTimeFormat(this.getLocales(o), o);
if (f1) { // not auto-formatting
// some formatters need some more work...
var dayEraVtz = "vVzGabB".indexOf(sig) >= 0; // day period, era or verbose timezone
if (dayEraVtz) {
var part = sdfO[sig][0];
if (hasF2P) {
if (part == 'period') part = 'dayperiod';
/** @this {Intl.DateTimeFormat} */
fn = function(part, d) {
var fParts = this.formatToParts(d),
r0 = fParts.find((pp) => pp['type'].toLowerCase() == part);
return r0 && r0['value'];
}.bind(dtf, part);
} else { // try to extract text from longer string
/** @this {Intl.DateTimeFormat} */
fn = function(d) {
var v0 = this.format(d);
// replace: all digits followed by non-words chars and whitespace
return v0.replace(/\s*\d+[^\w\s]*/g, '').trim();
}.bind(dtf);
}
} else { // check some numerics: hours, minutes or seconds - or years!
var showHours = 'hHkK'.indexOf(sig) >= 0;
// hour formatters are quite special - move out of here?
if (showHours) {
var fnGetHours = this.$dpFn.getHours;
fn = (d) => {
var h0 = fnGetHours.call(d);
// don't show 0 hours -> 12 or 24
if ((!h0 || h0 == 0) && (sig == 'H' || sig == 'K')) h0 = 24;
// restrict to 12 hour time
if (h0 > 12 && (sig == 'h' || sig == 'K')) h0 -= 12;
if (h0 == 0 && (sig == 'h' || sig == 'K')) h0 = 12;
// ev. pad to 2 digits
return (''+h0).padStart(pat.length, '0');
};
} else if ('sm'.indexOf(sig) >= 0) {
// minutes and seconds should be numbers - check this
// internet explorer has a problem with minutes and seconds
// see https://github.com/Microsoft/ChakraCore/issues/1223
if (isNaN(dtf.format(new Date()))) return null;
if (pat.length > 1) {
// 2-digit minutes or seconds
/** @this {Intl.DateTimeFormat} */
fn = function(d) {
var v = +this.format(d);
return ((v < 10) ? '0' : '') + v;
}.bind(dtf);
}
}
else if (pat.length == 5 && (sig == 'y' || sig == 'Y')) {
// ??? use NumberFormat if negative years are fixed upstream ???
/** @this {Intl.DateTimeFormat} */
fn = function(d) { return this.format(d).padStart(5, '0'); }.bind(dtf);
}
}
}
return fn || dtf.format.bind(dtf);
}
/**
* update DTF options for single SDF pattern (updateFmtOpts)
* @private
* @static
* @param {string} pat pattern
* @param {Object} o dtf options to construct
* @returns {Object} DTF options object (if pat was found)
*/
static sdf2dtfO(pat, o) {
// no DTF options for week
var sig = pat[0], noDtf = 'wW'.indexOf(sig) >= 0;
var pDef = !noDtf && sdfO[sig];
if (pDef) {
var prop = pDef[0], offset = pDef[1], idx;
if (offset && offset instanceof Function) {
idx = offset(pat);
} else {
idx = pat.length - 1;
if (offset) idx += offset;
var min = pDef[2];
if (min && idx < min) idx = min;
var max = pDef[3];
if (max && idx > max) idx = max;
}
o[prop] = dtfStyles[idx];
// not for iso8601 TZ
if (sig == 'x' || sig == 'X' || (sig == 'Z' && pat.length >= 3))
return null;
// any preference regarding 12/24 hour clock found?
if (hasHC && "hHkK".indexOf(sig) >= 0) {
o['hourCycle'] = sdfO['hc'][sig];
} else {
if (sdfO['hour12'].indexOf(sig) >= 0)
o['hour12'] = true;
else if (sdfO['no12h'].indexOf(sig) >= 0)
o['hour12'] = false;
}
// no period w/o hour
if (o['period'] && !o['hour'])
o['hour'] = 'numeric';
} else o = null;
return o;
}
/**
* get DTF options for auto-formatting
* @private
* @param {!string} pat input pattern
* @param {Object=} o options to update if given
* @returns {Object} options
*/
static dtfOptions(pat, o) {
if (!o) o = {};
var oRx = /([a-zA-Z])\1*/g, m;
// eslint-disable-next-line no-cond-assign
while (m = oRx.exec(pat)) { // m: [pat1, sig]
SimpleDateFormat.sdf2dtfO(m[0], o);
}
// 'hour12' would overwrite 'hourCycle', remove if both defined
if (o['hourCycle'] && 'hour12' in o) {
delete o['hour12'];
}
return o;
}
}
Intl.SimpleDateFormat = SimpleDateFormat;
/**
* formatter cache
* @private {Object<string, SimpleDateFormat>}
*/
const formatters = {};
/**
* for easier documentation using google closure compiler
* @typedef {function(!Date):!string}
*/
// eslint-disable-next-line no-unused-vars
var dateFmtFn;
/**
* helper data for {@see SimpleDateFormat#prepDateFns}
* @type {Array<string>}
*/
const dateFnParts = ['Date', 'Day', 'FullYear', 'Hours', 'Milliseconds', 'Minutes', 'Month', 'Seconds'];
// helper objects to decide on the right formatter
const dtfData = {};
const dtfStyles = dtfData.styles = ['numeric', '2-digit', 'short', 'long', 'narrow'],
sdfO = dtfData.sig = {}, sdfFnSig = dtfData.sigFn = {}, sdfFnPat = dtfData.patFn = {};
//--- era: G ------------------------------------------------------------
sdfO['G'] = ['era', 0, 2];
// year: yYuUr
sdfO['y'] = sdfO['Y'] = ['year', function(p) { return (p.length == 2) ? 1 : 0; }];
sdfO['u'] = sdfO['r'] = ["year"];
// ??? U: cyclic year name -> n/a
//--- quarter: Qq -------------------------------------------------------
sdfFnSig['Q'] = sdfFnSig['q'] = function(d, pat, utc) {
var m = utc ? d.getUTCMonth() : d.getMonth(), q = 1 + ~~(m/3), s;
var pads = ['', '0', 'Q', null, ''], l = pat.length;
if (l == 4) {
s = q + '. quarter'; // TODO: localize / use ordinal suffix
} else {
s = pads[l-1] + q;
}
return s;
};
//--- month: MLl --------------------------------------------------------
sdfO['M'] = sdfO['L'] = ['month'];
// ??? l - deprecated
//--- week: wW ----------------------------------------------------------
sdfO['w'] = ['week', 0, 0, 1];
sdfFnSig['w'] = function weekNumYear(d, pat, utc) { // week of year
var firstWeekday = 'sunday', // or monday ? -> locale dependent!
weekday = utc ? d.getUTCDay() : d.getDay();
// adjust weekday ???
if (firstWeekday === 'monday') {
if (weekday === 0) // Sunday
weekday = 6;
else
weekday--;
}
var d1 = utc ? new Date(Date.UTC(d.getUTCFullYear(), 0, 1)) : new Date(d.getFullYear(), 0, 1),
yday = Math.floor((d - d1) / 86400000),
weekNum = (yday + 7 - weekday) / 7;
return Math.floor(weekNum);
};
// eslint-disable-next-line no-unused-vars
sdfFnSig['W'] = (d, pat, utc) => { // week of month - first dirty approx
return '?'; // better none than wrong, this needs locale data <- TODO !!!
};
//--- day: dDFg ---------------------------------------------------------
sdfO['d'] = ['day', 0, 0, 1];
// D - day of year (num) -> %j
// ??? F - day of week in month (num), ie: "2" for "2nd Wed in July"
sdfFnSig['F'] = (d, pat, utc) => {
var dm = utc ? d.getUTCDate() : d.getDate();
return '' + (1 + ~~((dm-1)/7));
};
// g - modified julian day (num)
sdfFnSig['g'] = (d, pat, utc) => {
var d0 = (+d / 86400000);
if (!utc) d0 -= (d.getTimezoneOffset() / 1440);
return '' + ~~(d0 + 2440587.5);
};
//--- weekday: Eec ------------------------------------------------------
sdfO['E'] = ['weekday', 0, 2, 4];
sdfO['e'] = sdfO['c'] = ['weekday', 0, 2, 5]; // TODO: 'numeric' and '2-digit' not valid here ???
// c, cc, e -> numeric: 1 digit
sdfFnPat['c'] = sdfFnPat['cc'] = sdfFnPat['e'] = (d, pat1, utc) => {
return '' + ((utc ? d.getUTCDay() : d.getDay()) || 7);
};
// ee -> numeric: 2 digits (0pad)
sdfFnPat['ee'] = (d, pat1, utc) => {
return '0' + ((utc ? d.getUTCDay() : d.getDay()) || 7);
};
//--- period: abB -------------------------------------------------------
sdfO['a'] = sdfO['b'] = sdfO['B'] = ['period'];
//--- hour: hHKkjJC -----------------------------------------------------
sdfO['h'] = sdfO['H'] = sdfO['k'] = sdfO['K'] = sdfO['j'] = sdfO['J'] = sdfO['C'] = ['hour', 0, 0, 1];
//--- minute: m ---------------------------------------------------------
sdfO['m'] = ['minute', 0, 0, 1];
//--- second: sSA -------------------------------------------------------
sdfO['s'] = ['second', 0, 0, 1];
// eslint-disable-next-line no-unused-vars
sdfFnSig['S'] = (d, pat, utc) => { // fractional second
var ms1 = 1000 + d.getMilliseconds(), l = pat.length, s0 = '' + ms1;
if (l > 3) s0 = s0.padEnd(l+1, '0');
return s0.substr(1, l);
};
sdfFnSig['A'] = (d, pat, utc) => { // milliseconds in day
var ms = +d;
if (utc) ms -= (d.getTimezoneOffset() * 60000); // TZ offset: minutes -> ms
return ('' + (ms % 86400000)).padStart(pat.length, '0');
};
// sep ???
//--- zone: zZOvVXx -----------------------------------------------------
const iso8601tz = (d, utc, opts) => {
var tzo = utc ? 0 : d.getTimezoneOffset(), res = '';
if (opts.z0 && !tzo) return 'Z';
if (opts.gmt) {
res = 'GMT';
if (!tzo) return res;
}
// add sign and hours
res += (tzo > 0) ? '-' : '+'; // strange: tzo < 0 -> GMT+xxx
if (tzo < 0) // sign handled, make calc easier
tzo = -tzo;
var h = ~~(tzo/60), m = 0, s = 0;
res += ((opts.h2 && h < 10) ? '0' : '') + h; // hours
// minutes
m = tzo % 60;
s = ~~(m * 60);
if (!opts.noM0 || m || s) {
if (opts.sep) res += ':';
res += ((m < 10) ? '0' : '') + m;
if (opts.secs) {
if (s) {
if (opts.sep) res += ':';
res += ((s < 10) ? '0' : '') + s;
}
}
}
return res;
};
sdfO['z'] = sdfO['Z'] = sdfO['v'] = ['timeZoneName', 0, 2, 3];
sdfFnSig['Z'] = (d, pat, utc) => {
var pl = pat.length, l4 = pl == 4;
var opts = {gmt:l4, h2:!l4, sep: pl>=4, secs: pl>=5, z0: pl>=5};
return iso8601tz(d, utc, opts);
};
sdfFnSig['O'] = (d, pat, utc) => {
// all length but 1 and 4 should be an error
var pl = pat.length, opts = {gmt:true, h2: pl>=3, noM0: pl<3, sep:pl>=3};
return iso8601tz(d, utc, opts);
};
sdfO['V'] = ['timeZoneName', 0, 3, 3]; // always "long"
sdfO['X'] = sdfO['x'] = ['timeZoneName', 0, 2, 2]; // always "short"
sdfFnSig['X'] = sdfFnSig['x'] = (d, pat, utc) => {
var pl = pat.length, opts = {
h2: true, sep: pl>1 && pl&1, noM0: pl==1, secs: pl >= 4, z0: pat[0] == 'X'
};
return iso8601tz(d, utc, opts);
};
//-----------------------------------------------------------------------
// which formats suggest 12 hour clock?
sdfO['hour12'] = 'abBhK';
// and which one don't want one?
sdfO['no12h'] = 'Hk';
// hour cycles - K:h11 h:h12 H:h23 k:h24
sdfO['hc'] = {"K":"h11", "h":"h12", "H":"h23", "k":"h24"};
let testF = new Intl.DateTimeFormat('en', {'hour':'numeric','hourCycle':'h23'});
const hasF2P = !!testF['formatToParts'];
const hasHC = testF.resolvedOptions()['hourCycle'] == 'h23';

View File

@@ -0,0 +1,137 @@
import Gio from 'gi://Gio'
function asGioPromise(obj, methodStart, methodFinish = undefined) {
methodFinish =
methodFinish ??
methodStart.replace('_async', '').replace('_begin', '') + '_finish'
return function (...args) {
return new Promise((resolve, reject) => {
let { stack: callStack } = new Error()
this[methodStart](...args, function (source, res) {
try {
const result =
source !== null && source[methodFinish] !== undefined
? source[methodFinish](res)
: obj[methodFinish](res)
if (Array.isArray(result) && result.length > 1 && result[0] === true)
result.shift()
resolve(result)
} catch (error) {
callStack = callStack
.split('\n')
.filter((line) => line.indexOf('_promisify/') === -1)
.join('\n')
if (error.stack)
error.stack += `### Promise created here: ###\n${callStack}`
else error.stack = callStack
reject(error)
}
})
})
}.bind(obj)
}
const URI_SCHEMAS = Gio.Vfs.get_local()
.get_supported_uri_schemes()
.map((schema) => `${schema}://`)
export function isUri(uri) {
const regex = /^([a-z]+:\/\/)/i
return regex.test(uri)
}
export function isSupportedUri(uri) {
for (const schema of URI_SCHEMAS) {
if (uri.startsWith(schema)) return true
}
return false
}
function allowedExtension(name, extensions) {
for (const ext of extensions) {
if (name.endsWith(`.${ext}`)) return true
}
return false
}
function removeExtension(name, extensions) {
let fname = name
for (const ext of extensions) fname = fname.replaceAll(`.${ext}`, '')
return fname
}
async function children(directory) {
const ret = []
const children = await asGioPromise(directory, 'enumerate_children_async')(
Gio.FILE_ATTRIBUTE_STANDARD_NAME,
Gio.FileQueryInfoFlags.NONE,
0,
null
)
let child = children.next_file(null)
while (child) {
ret.push({
name: child.get_name(),
type: Object.keys(Gio.FileType)[child.get_file_type()],
})
child = children.next_file(null)
}
return ret
.filter(
({ type }) =>
type !== Gio.FileType.REGULAR && type !== Gio.FileType.UNKNOWN
)
.map((c) => c.name)
}
export function resolveFile(uri, ...paths) {
//the code work for most cases
let file
if (isUri(uri)) {
file = Gio.File.new_for_uri(isSupportedUri(uri) ? uri : import.meta.url)
} else if (uri.startsWith('/')) {
file = Gio.File.new_for_path(uri)
} else {
file = Gio.File.new_for_uri(import.meta.url)
paths.push(uri)
}
const fileInfo = file.query_info(
Gio.FILE_ATTRIBUTE_STANDARD_NAME + ',' + Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
Gio.FileQueryInfoFlags.NONE,
null
)
if (fileInfo.get_file_type() === Gio.FileType.REGULAR) {
file = file.get_parent()
}
if (paths.length > 0) {
for (const path of paths) {
file = file.resolve_relative_path(path)
}
}
return file
}
export function resolvePath(...paths) {
const file = resolveFile(...paths)
return file.get_uri() === import.meta.url ? '' : file.get_path()
}
export function resolveUri(...paths) {
const file = resolveFile(...paths)
return file.get_uri() === import.meta.url ? '' : file.get_uri()
}
export async function importDir(
dir,
excludeIndex = false,
enabledExtension = ['js', 'ts']
) {
const res = {}
dir = Array.isArray(dir) ? dir : [dir]
const directory = resolveFile(...dir)
const files = await children(directory)
for (const file of files) {
if (allowedExtension(file, enabledExtension)) {
const name = removeExtension(file, enabledExtension)
if (!(excludeIndex && name.toLowerCase() === 'index')) {
res[name] = await import(`${directory.get_uri()}/${file}`)
}
}
}
return res
}

View File

@@ -0,0 +1,14 @@
{
"_generated": "Generated by SweetTooth, do not edit",
"description": "Allows customization of the date display in the panel.\n\nMight be especially useful if you're using a horizontal panel which does not at all work well with the default date display.\n\nCHANGELOG\nVersion 5: added support for multiple Dash To Panel panels\nVersion 6: fixed issues on earlier Gnome Shell versions\nVersion 10: fixed clock hover style (by bomdia)\nVersion 11: Gnome 45 update by andyholmes@github\nVersion 12: added support for advanced formatters by bomdia@github\nVersion 15: added text alignment choice by bomdia@github",
"gettext-domain": "date-menu-formatter",
"name": "Date Menu Formatter",
"settings-schema": "org.gnome.shell.extensions.date-menu-formatter",
"shell-version": [
"45",
"46"
],
"url": "https://github.com/marcinjakubowski/date-menu-formatter",
"uuid": "date-menu-formatter@marcinjakubowski.github.com",
"version": 15
}

View File

@@ -0,0 +1,558 @@
/* extension.js
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/*
Swaths of pref related code borrowed from Clipboard Indicator, an amazing extension
https://github.com/Tudmotu/gnome-shell-extension-clipboard-indicator
https://extensions.gnome.org/extension/779/clipboard-indicator/
*/
import Gio from 'gi://Gio'
import Gtk from 'gi://Gtk?version=4.0'
import Adw from 'gi://Adw'
import {
ExtensionPreferences,
gettext as _,
} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'
import * as prefFields from './utils/prefFields.js'
import {
getCurrentCalendar,
getCurrentTimezone,
getCurrentLocale,
updateLevelToString,
TEXT_ALIGN_START,
TEXT_ALIGN_CENTER,
TEXT_ALIGN_END,
} from './utils/general.js'
import { useAddRow, createLabel, addBox, table, a, b } from './utils/markup.js'
import { CALENDAR_LIST, FormatterManager } from './utils/formatter.js'
class Preferences {
constructor(settings) {
this.settings = settings
this.formatters = new FormatterManager()
this._previewErrorCount = 0
this.box = {}
this.initUI()
this.formatters
.loadFormatters()
.then(() => {
this.createUI()
this.UIShowHideFormatterAbility(
this.formatters.getFormatter(this._formatter.active_id).can
)
this.generatePreview()
})
.catch((e) => {
console.error('Date Menu Formatter error:', e)
})
}
initUI() {
this.main = new Gtk.Grid({
margin_top: 10,
margin_bottom: 10,
margin_start: 10,
margin_end: 10,
row_spacing: 12,
column_spacing: 18,
column_homogeneous: false,
row_homogeneous: false,
})
this.addRow = useAddRow(this.main)
this.addSeparator = () => this.addRow(null, new Gtk.Separator())
}
createUI() {
this.UIcreateFormatterSetting()
this.UIcreateFontSizeSetting()
this.UIcreateTextAlignSetting()
this.UIcreatePatternSetting()
this.UIcreatePatternPreview()
this.addSeparator()
this.UIcreateUpdateLevelSetting()
this.UIcreateDefaultLocaleSetting()
this.UIcreateDefaultCalendarSetting()
this.UIcreateDefaultTimezoneSetting()
this.addSeparator()
this.UIcreateRemoveUnreadMessagesSetting()
this.UIcreateAllPanelsSetting()
this.addSeparator()
this.UIcreateFormatterHelp()
}
UIcreateFormatterSetting() {
const formatterSelect = new Gtk.ComboBoxText({
hexpand: true,
halign: Gtk.Align.FILL,
})
this.formatters.asList().forEach(({ key, name }) => {
formatterSelect.append(key, name)
})
formatterSelect.set_active_id(
this.settings.get_string(prefFields.FORMATTER)
)
this._formatter = formatterSelect
this.addRow(createLabel(_('Formatter')), formatterSelect)
this.settings.bind(
prefFields.FORMATTER,
formatterSelect,
'active-id',
Gio.SettingsBindFlags.DEFAULT
)
formatterSelect.connect('changed', () => {
this.setHelpMarkup(
this.formatters.getFormatterHelp(this._formatter.active_id)
)
this.UIShowHideFormatterAbility(
this.formatters.getFormatter(this._formatter.active_id).can
)
this.generatePreview()
})
}
UIShowHideFormatterAbility(can) {
this.box.locale(can.customLocale)
this.box.calendar(can.customCalendar)
this.box.timezone(can.customTimezone)
}
UIcreatePatternSetting() {
const patternEdit = new Gtk.Entry({ buffer: new Gtk.EntryBuffer() })
this.addRow(createLabel(_('Pattern')), patternEdit)
this._pattern = patternEdit.buffer
this.settings.bind(
prefFields.PATTERN,
patternEdit.buffer,
'text',
Gio.SettingsBindFlags.DEFAULT
)
patternEdit.buffer.connect_after(
'inserted-text',
this.generatePreview.bind(this)
)
patternEdit.buffer.connect_after(
'deleted-text',
this.generatePreview.bind(this)
)
}
UIcreatePatternPreview() {
this._preview = createLabel('')
this._preview.set_use_markup(true)
this.addRow(createLabel(_('Preview')), this._preview)
}
UIcreateUpdateLevelSetting() {
const updateLevelSelect = new Gtk.ComboBoxText({
hexpand: true,
halign: Gtk.Align.FILL,
})
for (let i = 0; i <= 15; i++) {
updateLevelSelect.append('' + i, updateLevelToString(i))
}
updateLevelSelect.set_active_id(
'' + this.settings.get_int(prefFields.UPDATE_LEVEL)
)
this.addRow(createLabel(_('Update')), updateLevelSelect)
updateLevelSelect.connect('changed', () => {
this.settings.set_int(
prefFields.UPDATE_LEVEL,
parseInt(updateLevelSelect.active_id)
)
})
}
UIcreateDefaultLocaleSetting() {
const useDefaultLocaleLabel = createLabel(
_('Use default locale') + ` (${getCurrentLocale()})`
)
const localeBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 30,
})
const useDefaultLocaleEdit = new Gtk.Switch({
vexpand: false,
valign: Gtk.Align.CENTER,
})
const customLocaleEdit = new Gtk.Entry({ buffer: new Gtk.EntryBuffer() })
addBox(localeBox, useDefaultLocaleEdit)
addBox(localeBox, customLocaleEdit)
this.addRow(useDefaultLocaleLabel, localeBox)
this.box.locale = (show) => {
if (show) {
localeBox.show()
useDefaultLocaleLabel.show()
} else {
localeBox.hide()
useDefaultLocaleLabel.hide()
}
}
this._customLocale = customLocaleEdit.buffer
this._useDefaultLocale = useDefaultLocaleEdit
this.settings.bind(
prefFields.USE_DEFAULT_LOCALE,
useDefaultLocaleEdit,
'active',
Gio.SettingsBindFlags.DEFAULT
)
this.settings.bind(
prefFields.USE_DEFAULT_LOCALE,
customLocaleEdit,
'sensitive',
Gio.SettingsBindFlags.GET |
Gio.SettingsBindFlags.NO_SENSITIVITY |
Gio.SettingsBindFlags.INVERT_BOOLEAN
)
this.settings.bind(
prefFields.CUSTOM_LOCALE,
customLocaleEdit.buffer,
'text',
Gio.SettingsBindFlags.DEFAULT
)
useDefaultLocaleEdit.connect('state-set', this.generatePreview.bind(this))
customLocaleEdit.buffer.connect_after(
'inserted-text',
this.generatePreview.bind(this)
)
customLocaleEdit.buffer.connect_after(
'deleted-text',
this.generatePreview.bind(this)
)
}
UIcreateDefaultCalendarSetting() {
const defaultCalendarName = CALENDAR_LIST.find(
({ key }) => key === getCurrentCalendar()
).name
const useDefaultCalendarLabel = createLabel(
_('Use default calendar') + ` (${defaultCalendarName})`
)
const calendarBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 30,
})
const useDefaultCalendarEdit = new Gtk.Switch({
vexpand: false,
valign: Gtk.Align.CENTER,
})
const customCalendarSelect = new Gtk.ComboBoxText({
hexpand: false,
halign: Gtk.Align.FILL,
})
CALENDAR_LIST.forEach(({ key, name, description }) => {
customCalendarSelect.append(key, `${name} -> "${description}"`)
})
customCalendarSelect.set_active_id(
this.settings.get_string(prefFields.CUSTOM_CALENDAR) ||
getCurrentCalendar()
)
addBox(calendarBox, useDefaultCalendarEdit)
addBox(calendarBox, customCalendarSelect)
this.addRow(useDefaultCalendarLabel, calendarBox)
this.box.calendar = (show) => {
if (show) {
calendarBox.show()
useDefaultCalendarLabel.show()
} else {
calendarBox.hide()
useDefaultCalendarLabel.hide()
}
}
this._customCalendar = customCalendarSelect
this._useDefaultCalendar = useDefaultCalendarEdit
this.settings.bind(
prefFields.USE_DEFAULT_CALENDAR,
useDefaultCalendarEdit,
'active',
Gio.SettingsBindFlags.DEFAULT
)
this.settings.bind(
prefFields.CUSTOM_CALENDAR,
customCalendarSelect,
'active-id',
Gio.SettingsBindFlags.DEFAULT
)
this.settings.bind(
prefFields.USE_DEFAULT_CALENDAR,
customCalendarSelect,
'sensitive',
Gio.SettingsBindFlags.GET |
Gio.SettingsBindFlags.NO_SENSITIVITY |
Gio.SettingsBindFlags.INVERT_BOOLEAN
)
useDefaultCalendarEdit.connect('state-set', this.generatePreview.bind(this))
customCalendarSelect.connect('changed', this.generatePreview.bind(this))
}
UIcreateDefaultTimezoneSetting() {
const useDefaultTimezoneLabel = createLabel(
_('Use default timezone') + ` (${getCurrentTimezone()})`
)
const timezoneBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 30,
})
const useDefaultTimezoneEdit = new Gtk.Switch({
vexpand: false,
valign: Gtk.Align.CENTER,
})
const customTimezoneEdit = new Gtk.Entry({ buffer: new Gtk.EntryBuffer() })
addBox(timezoneBox, useDefaultTimezoneEdit)
addBox(timezoneBox, customTimezoneEdit)
this.addRow(useDefaultTimezoneLabel, timezoneBox)
this.box.timezone = (show) => {
if (show) {
timezoneBox.show()
useDefaultTimezoneLabel.show()
} else {
timezoneBox.hide()
useDefaultTimezoneLabel.hide()
}
}
this._customTimezone = customTimezoneEdit.buffer
this._useDefaultTimezone = useDefaultTimezoneEdit
this.settings.bind(
prefFields.USE_DEFAULT_TIMEZONE,
useDefaultTimezoneEdit,
'active',
Gio.SettingsBindFlags.DEFAULT
)
this.settings.bind(
prefFields.CUSTOM_TIMEZONE,
customTimezoneEdit.buffer,
'text',
Gio.SettingsBindFlags.DEFAULT
)
this.settings.bind(
prefFields.USE_DEFAULT_TIMEZONE,
customTimezoneEdit,
'sensitive',
Gio.SettingsBindFlags.GET |
Gio.SettingsBindFlags.NO_SENSITIVITY |
Gio.SettingsBindFlags.INVERT_BOOLEAN
)
useDefaultTimezoneEdit.connect('state-set', this.generatePreview.bind(this))
customTimezoneEdit.buffer.connect_after(
'inserted-text',
this.generatePreview.bind(this)
)
customTimezoneEdit.buffer.connect_after(
'deleted-text',
this.generatePreview.bind(this)
)
}
UIcreateRemoveUnreadMessagesSetting() {
const removeMessagesIndicatorEdit = new Gtk.Switch()
this.addRow(
createLabel(_('Remove unread messages indicator')),
removeMessagesIndicatorEdit
)
this.settings.bind(
prefFields.REMOVE_MESSAGES_INDICATOR,
removeMessagesIndicatorEdit,
'active',
Gio.SettingsBindFlags.DEFAULT
)
}
UIcreateAllPanelsSetting() {
const applyAllPanelsEdit = new Gtk.Switch()
this.addRow(
createLabel(_('Apply to all panels (Dash to Panel)')),
applyAllPanelsEdit
)
this.settings.bind(
prefFields.APPLY_ALL_PANELS,
applyAllPanelsEdit,
'active',
Gio.SettingsBindFlags.DEFAULT
)
}
UIcreateFontSizeSetting() {
const fontSizeEdit = new Gtk.SpinButton({
adjustment: new Gtk.Adjustment({
lower: 4,
upper: 30,
step_increment: 1,
}),
})
this.addRow(createLabel(_('Font size')), fontSizeEdit)
fontSizeEdit.connect(
'output',
function (spin) {
spin.text = `${spin.value} pt`
this.FONT_SIZE = spin.value
return true
}.bind(this)
)
this.settings.bind(
prefFields.FONT_SIZE,
fontSizeEdit,
'value',
Gio.SettingsBindFlags.DEFAULT
)
}
UIcreateTextAlignSetting() {
const tAlignBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 0,
})
tAlignBox.set_css_classes(['linked'])
const buttons = [
{
btn: Gtk.Button.new_from_icon_name('format-justify-left-symbolic'),
key: TEXT_ALIGN_START,
},
{
btn: Gtk.Button.new_from_icon_name('format-justify-center-symbolic'),
key: TEXT_ALIGN_CENTER,
},
{
btn: Gtk.Button.new_from_icon_name('format-justify-right-symbolic'),
key: TEXT_ALIGN_END,
},
]
const settings = this.settings
const selected = {
get value() {
return settings.get_string(prefFields.TEXT_ALIGN)
},
set value(sel) {
buttons.forEach(({ btn, key }) => {
btn.set_sensitive(key !== sel)
})
settings.set_string(prefFields.TEXT_ALIGN, sel)
},
}
selected.value = selected.value || TEXT_ALIGN_CENTER
buttons.forEach(({ btn, key }) => {
addBox(tAlignBox, btn)
btn.connect('clicked', function () {
selected.value = key
})
})
this.addRow(createLabel(_('Align Text')), tAlignBox)
}
UIcreateFormatterHelp() {
const left = createLabel('')
const right = createLabel('')
this.setHelpMarkup = (help) => {
left.set_markup(`${b('Available pattern components')}${table(help.left)}`)
right.set_markup(`${a(help.link, 'Full list (web)')}${table(help.right)}`)
}
this.setHelpMarkup(
this.formatters.getFormatterHelp(this._formatter.active_id)
)
this.addRow(left, right)
}
generatePreview() {
const locale = this._useDefaultLocale.active
? getCurrentLocale()
: this._customLocale.text
const calendar = this._useDefaultCalendar.active
? getCurrentCalendar()
: this._customCalendar.active_id
const timezone = this._useDefaultTimezone.active
? getCurrentTimezone()
: this._customTimezone.text
if (this._pattern.text.length > 1) {
try {
const formatter = this.formatters.getFormatter(
this._formatter.active_id
)
this._preview.label = new formatter(timezone, locale, calendar).format(
this._pattern.text,
new Date()
)
this._previewErrorCount = 0
} catch (e) {
this._previewErrorCount++
if (this._previewErrorCount > 2) {
this._preview.label = 'ERROR: ' + e.message
}
}
} else {
this._preview.label = ''
this._previewErrorCount = 0
}
}
}
export default class DateMenuFormatterPreferences extends ExtensionPreferences {
getPreferencesWidget() {
const frame = new Gtk.Box()
const widget = new Preferences(this.getSettings())
addBox(frame, widget.main)
if (frame.show_all) frame.show_all()
return frame
}
fillPreferencesWindow(window) {
window._settings = this.getSettings()
window.set_size_request(1000, 700)
const page = new Adw.PreferencesPage()
const group = new Adw.PreferencesGroup({
title: _('General'),
})
group.add(this.getPreferencesWidget())
page.add(group)
window.add(page)
}
}

View File

@@ -0,0 +1,91 @@
<schemalist gettext-domain="date-menu-formatter">
<schema id="org.gnome.shell.extensions.date-menu-formatter"
path="/org/gnome/shell/extensions/date-menu-formatter/">
<key type="s" name="formatter">
<default>"01_luxon"</default>
<summary>Date formatter</summary>
<description></description>
</key>
<key type="s" name="pattern">
<default>"EEE, MMM d H : mm"</default>
<summary>Date format pattern</summary>
<description></description>
</key>
<key type="s" name="custom-locale">
<default>""</default>
<summary>Custom locale</summary>
<description></description>
</key>
<key type="b" name="use-default-locale">
<default>true</default>
<summary>Should default system locale be used</summary>
<description></description>
</key>
<key type="s" name="custom-calendar">
<default>""</default>
<summary>Custom Calendar</summary>
<description></description>
</key>
<key type="b" name="use-default-calendar">
<default>true</default>
<summary>Should default calendar be used</summary>
<description></description>
</key>
<key type="s" name="custom-timezone">
<default>""</default>
<summary>Custom timezone</summary>
<description></description>
</key>
<key type="b" name="use-default-timezone">
<default>true</default>
<summary>Should default system timezone be used</summary>
<description></description>
</key>
<key type="b" name="remove-messages-indicator">
<default>false</default>
<summary>Should unread messages indicator be removed</summary>
<description></description>
</key>
<key type="b" name="apply-all-panels">
<default>false</default>
<summary>Should extension modify all Dash To Panel panels</summary>
<description></description>
</key>
<key type="i" name="font-size">
<default>10</default>
<summary>Font size</summary>
<description></description>
</key>
<key type="i" name="update-level">
<default>1</default>
<summary>Update Clock Every</summary>
<description>
0 = every minute
1 = every seconds
2 = 2 time in a second
3 = 3 time in a second
4 = 4 time in a second
5 = 5 time in a second
6 = 6 time in a second
7 = 7 time in a second
8 = 8 time in a second
9 = 9 time in a second
10 = 10 time in a second
11 = 11 time in a second
12 = 12 time in a second
13 = 13 time in a second
14 = 14 time in a second
15 = 15 time in a second
</description>
</key>
<key type="s" name="text-align">
<default>"center"</default>
<summary>Align the label</summary>
<description>
left
center
right
</description>
</key>
</schema>
</schemalist>

View File

@@ -0,0 +1,27 @@
var PrefFields = {
PATTERN : 'pattern',
USE_DEFAULT_LOCALE : 'use-default-locale',
CUSTOM_LOCALE : 'custom-locale',
FONT_SIZE : 'font-size',
APPLY_ALL_PANELS : 'apply-all-panels',
REMOVE_MESSAGES_INDICATOR: 'remove-messages-indicator'
};
function getCurrentLocale() {
return (new Intl.DateTimeFormat()).resolvedOptions().locale
}
function convertToPattern(str) {
return '#' + str.replace(new RegExp("\\\\n", "g"), "\n").replace(new RegExp("''", "g"), ">`<")
}
function convertFromPattern(str) {
return str.replace(new RegExp('>`<', "g"), "'")
}
export {
PrefFields,
getCurrentLocale,
convertToPattern,
convertFromPattern,
};

View File

@@ -0,0 +1,108 @@
import { importDir } from '../lib/importDir.js'
export class BaseFormatter {
constructor(timezone, locale, calendar) {
this.config(timezone, locale, calendar)
}
config(timezone, locale, calendar) {}
format(pattern, date) {}
}
export function createFormatter(
formatterLabel,
formatterDescription,
{ customTimezone, customLocale, customCalendar } = {}
) {
return class CustomFormatter extends BaseFormatter {
static label = formatterLabel || ''
static description = formatterDescription || ''
static can = Object.freeze({
customTimezone: !!customTimezone,
customLocale: !!customLocale,
customCalendar: !!customCalendar,
})
}
}
function freeze(arr) {
if (!Array.isArray(arr)) return Object.freeze([])
for (let i = 0; i < arr.length; i++) {
arr[i] = Object.freeze(arr[i])
}
return Object.freeze(arr)
}
export class FormatterHelp {
#link
#left
#right
constructor(link, left, right) {
this.#link = typeof link === 'string' && link !== '' ? link : ''
this.#left = freeze(left)
this.#right = freeze(right)
}
get link() {
return this.#link
}
get left() {
return this.#left
}
get right() {
return this.#right
}
}
export class FormatterManager {
constructor(load = false) {
this.formatters = {}
if (load) this.loadFormatters()
}
async loadFormatters() {
this.formatters = await importDir([import.meta.url, '../formatters'])
return this.formatters
}
getFormatter(key) {
return this.formatters[key] ? this.formatters[key].default : undefined
}
getFormatterHelp(key) {
return this.formatters[key] ? this.formatters[key].help() : undefined
}
asList() {
return Object.keys(this.formatters)
.filter((f) => !f.startsWith('_'))
.map((f) => ({
key: f,
name: this.getFormatter(f).label,
description: this.getFormatter(f).description,
}))
}
}
export const CALENDAR_LIST = [
{ key: 'gregory', description: '1 March 2010', name: 'Gregorian' },
{ key: 'buddhist', description: 'September 24, 2560 BE', name: 'Buddhist' },
{ key: 'chinese', description: 'Eighth Month 5, 2017', name: 'Chinese' },
{ key: 'coptic', description: 'Tout 14, 1734 ERA1', name: 'Coptic' },
{
key: 'ethioaa',
description: 'Meskerem 14, 7510 ERA0',
name: 'Ethiopic (Amete Alem)',
},
{ key: 'ethiopic', description: 'Meskerem 14, 2010 ERA1', name: 'Ethiopic' },
{ key: 'hebrew', description: '4 Tishri 5778', name: 'Hebrew' },
{ key: 'indian', description: 'Asvina 2, 1939 Saka', name: 'Indian' },
{ key: 'islamic', description: 'Muharram 4, 1439 AH', name: 'Islamic' },
{
key: 'islamic-civil',
description: 'Muharram 3, 1439 AH',
name: 'Islamic Civil',
},
{ key: 'iso8601', description: 'September 24, 2017', name: 'ISO 8601' },
{ key: 'japanese', description: 'September 24, 29 Heisei', name: 'Japanase' },
{ key: 'persian', description: 'Mehr 2, 1396 AP', name: 'Persian' },
{ key: 'roc', description: 'September 24, 106 Minguo', name: 'Minguo' },
]

View File

@@ -0,0 +1,40 @@
import GLib from 'gi://GLib'
export const TEXT_ALIGN_START = 'left'
export const TEXT_ALIGN_CENTER = 'center'
export const TEXT_ALIGN_END = 'right'
function currentOptions() {
return new Intl.DateTimeFormat().resolvedOptions()
}
export function getCurrentLocale() {
return currentOptions().locale
}
export function getCurrentTimezone() {
return currentOptions().timeZone
}
export function getCurrentCalendar() {
return currentOptions().calendar
}
export function updateLevel(lvl) {
if (typeof lvl === 'number' && !Number.isNaN(lvl)) {
if (lvl === 0)
return { lvl, priority: GLib.PRIORITY_DEFAULT_IDLE, timeout: 1000 * 60 }
if (lvl > 0 && lvl <= 7)
return { lvl, priority: GLib.PRIORITY_DEFAULT, timeout: 1000 / lvl }
if (lvl > 7 && lvl <= 15)
return { lvl, priority: GLib.PRIORITY_HIGH, timeout: 1000 / lvl }
}
return { lvl: 1, priority: GLib.PRIORITY_DEFAULT, timeout: 1000 }
}
export function updateLevelToString(lvl) {
if (typeof lvl === 'number' && !Number.isNaN(lvl)) {
if (lvl === 0) return `every minute`
if (lvl === 1) return `every second`
if (lvl > 1 && lvl <= 15) return `${lvl} times in a second`
}
return `every second`
}

View File

@@ -0,0 +1,72 @@
import Gtk from 'gi://Gtk'
import { gettext as _ } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'
function findPadSize(rows) {
let maxSize = []
for (const row of rows) {
row.forEach((col, i) => {
if (typeof maxSize[i] !== 'number') maxSize[i] = 0
if (col.length > maxSize[i]) maxSize[i] = col.length
})
}
return maxSize
}
function row(value, desc, ex) {
return `|<tt><b>${value}</b></tt> | <tt>${_(desc)}</tt> | <tt><i>${_(
ex
)}</i></tt>\n`
}
export function table(rows) {
const [patternPad, descriptionPad, examplePad] = findPadSize(rows)
return `\n\n${rows.reduce(
(acc, [pattern, description, example]) =>
acc +
row(
pattern.padEnd(patternPad),
description.padEnd(descriptionPad),
example.padStart(examplePad)
),
''
)}`
}
export function a(ref, label) {
return ref ? `<a href="${ref}">${_(label)}</a>` : ''
}
export function b(label) {
return `<b>${_(label)}</b>`
}
export function addBox(box, child) {
box.append(child)
}
export function useAddRow(main) {
let row = 0
return (label, input) => {
let inputWidget = input
if (input instanceof Gtk.Switch) {
inputWidget = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL })
addBox(inputWidget, input)
}
if (label) {
main.attach(label, 0, row, 1, 1)
if (inputWidget) main.attach(inputWidget, 1, row, 1, 1)
} else {
main.attach(inputWidget, 0, row, 2, 1)
}
row++
return row - 1
}
}
export function createLabel(label) {
return new Gtk.Label({
label: label,
hexpand: true,
halign: Gtk.Align.START,
})
}

View File

@@ -0,0 +1,13 @@
export const FORMATTER = 'formatter'
export const PATTERN = 'pattern'
export const UPDATE_LEVEL = 'update-level'
export const USE_DEFAULT_LOCALE = 'use-default-locale'
export const CUSTOM_LOCALE = 'custom-locale'
export const USE_DEFAULT_CALENDAR = 'use-default-calendar'
export const CUSTOM_CALENDAR = 'custom-calendar'
export const USE_DEFAULT_TIMEZONE = 'use-default-timezone'
export const CUSTOM_TIMEZONE = 'custom-timezone'
export const FONT_SIZE = 'font-size'
export const APPLY_ALL_PANELS = 'apply-all-panels'
export const REMOVE_MESSAGES_INDICATOR = 'remove-messages-indicator'
export const TEXT_ALIGN = 'text-align'