Skip to content

Commit

Permalink
✨ allow users to replace the favicon with a URL
Browse files Browse the repository at this point in the history
  • Loading branch information
Elliot67 committed Sep 24, 2023
1 parent 534b16a commit 02624b7
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 38 deletions.
50 changes: 31 additions & 19 deletions src/background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ browser.runtime.onInstalled.addListener((event): void => {

// Generate favicon for Options page
onMessage('get-favicon', async ({ data }) => {
const rule = data as unknown as AppDataRule;
const SETTINGS = await SettingsStorage.getItem();
return { favicon: await getNewFavicon(SETTINGS, rule) };
return { favicon: await getNewFavicon(data, [getFallbackFavicon(SETTINGS)]) };
});

// Generate favicon for the content script
Expand All @@ -28,24 +27,19 @@ onMessage('get-favicon-from-links', async ({ data: links, sender }) => {
return null;
}

links.push('FALLBACK_FAVICON');
links.push(getFallbackFavicon(SETTINGS));

for (const link of links) {
try {
const favicon = await getNewFavicon(SETTINGS, match, link);
return { favicon };
} catch (e) {
console.warn('error while genereting the favicon with this link, trying the next link', e);
}
try {
const favicon = await getNewFavicon(match, links);
return { favicon };
} catch (e) {
return null;
}

console.warn('Unexpected error, could not generate favicons from images or fallbacks', links);
return null;
});

function getMatch(
SETTINGS: AppDataGlobal,
prop: { url: string | undefined; title: string | undefined }
prop: { url: string | undefined; title: string | undefined },
): AppDataRule | false {
if (isUndefined(prop.url) && isUndefined(prop.title)) {
return false;
Expand All @@ -70,6 +64,28 @@ function getMatch(
return false;
}

async function getNewFavicon(rule: AppDataRule, links: string[]): Promise<string> {
console.log('what is the thing', rule);
if (rule.replacementType === 'external') {
return rule.externalFaviconLink;
}

for (const link of links) {
try {
const customizedFavicon = await getCustomizedFavicon(rule, link);
return customizedFavicon;
} catch (e) {
console.warn('error while genereting the favicon with this link, trying the next link', e);
}
}

throw new Error('Unexpected error, could not generate favicons from images or fallbacks', {
cause: {
links,
},
});
}

function getFallbackFavicon(SETTINGS: AppDataGlobal): string {
let type = SETTINGS.favicon.type;
if (type === 'custom') {
Expand All @@ -86,11 +102,7 @@ function getFallbackFavicon(SETTINGS: AppDataGlobal): string {
return baseFavicons[`${type}_${themeSuffix}`];
}

async function getNewFavicon(SETTINGS: AppDataGlobal, item: AppDataRule, url = 'FALLBACK_FAVICON'): Promise<string> {
if (url === 'FALLBACK_FAVICON') {
url = getFallbackFavicon(SETTINGS);
}

async function getCustomizedFavicon(item: AppDataRule, url: string): Promise<string> {
try {
const img = await loadImage(url);
const drawingParams = createCanvasWithImage(img);
Expand Down
13 changes: 13 additions & 0 deletions src/composables/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { SettingsStorage, getEmptyRule } from '~/logic';
import { AppDataGlobal, AppDataRule } from '~/types/app';
import { reactive, toRaw, watch } from 'vue';
import { defaultSettings } from '~/configuration/settings';
import { migrateSettings } from '~/logic/settings-migrations';
import { isDef, isString } from '~/utils';

export default function useSettings() {
const settings = reactive<AppDataGlobal>({ ...defaultSettings });

async function load(): Promise<void> {
const storageSettings = await SettingsStorage.getItem();
if (storageSettings.version !== defaultSettings.version) {
migrateSettings(storageSettings);
}
settings.version = storageSettings.version;
settings.favicon = storageSettings.favicon;
settings.rules = storageSettings.rules;
Expand All @@ -27,7 +32,14 @@ export default function useSettings() {
save();
});

function areSettingsPartiallyValid(settings: any): boolean {
return isString(settings.version) && isDef(settings.favicon) && Array.isArray(settings.rules);
}

async function importSettings(data: any): Promise<void> {
if (data.version !== defaultSettings.version) {
migrateSettings(data);
}
settings.version = data.version;
settings.favicon = data.favicon;
settings.rules = data.rules;
Expand Down Expand Up @@ -57,6 +69,7 @@ export default function useSettings() {
settings,
load,
save,
areSettingsPartiallyValid,
importSettings,
reset,

Expand Down
2 changes: 1 addition & 1 deletion src/configuration/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AppDataGlobal, IconsTypeFull } from '~/types/app';

const buildDefaultSettings = <T extends AppDataGlobal>(value: T) => value;
export const defaultSettings = buildDefaultSettings({
version: '1.0',
version: browser.runtime.getManifest().version,
favicon: {
type: 'earth',
custom: '',
Expand Down
2 changes: 2 additions & 0 deletions src/logic/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export function getEmptyRule(): AppDataRule {
active: true,
type: 'url',
testPattern: '.',
replacementType: 'generated',
filter: 'cover',
color: '#ff00ff',
externalFaviconLink: '',
};
}
25 changes: 25 additions & 0 deletions src/logic/settings-migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AppDataGlobal } from '~/types/app';

export function migrateSettings(settings: AppDataGlobal) {
for (const migration of migrationScripts) {
if (migration.fromVersion !== settings.version) {
continue;
}

migration.migrate(settings);
}
}

const migrationScripts: { fromVersion: string; toVersion: string; migrate: (settings: AppDataGlobal) => void }[] = [
{
fromVersion: '1.0',
toVersion: '1.2.0',
migrate: (settings) => {
settings.version = '1.2.0';
settings.rules.forEach((rule) => {
rule.replacementType = 'generated';
rule.externalFaviconLink = '';
});
},
},
];
99 changes: 84 additions & 15 deletions src/options/Options.vue
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,12 @@
<div v-if="selectedRuleId === rule.id" class="rules-edit">
<div class="ruleItem">
<h4 class="sectionLabel">Match pattern against</h4>
<p class="sectionItemDescription">Which element of the webpage will be used to match the pattern.</p>
<EsfRadioGroup v-model="rule.type" :options="ruleMatchPatternOptions"></EsfRadioGroup>
</div>
<div class="ruleItem">
<h4 class="sectionLabel">Pattern</h4>
<h4 class="sectionLabel">Regex</h4>
<p class="sectionItemDescription">The regex pattern determines if the rule will be applied.</p>
<EsfInputText
v-model="rule.testPattern"
label="Match pattern"
Expand All @@ -117,13 +119,33 @@
/>
</div>
<div class="ruleItem">
<h4 class="sectionLabel">Color</h4>
<EsfInputColor v-model="rule.color" label="Color code" placeholder="#663399" />
</div>
<div class="ruleItem">
<h4 class="sectionLabel">Overlay type</h4>
<EsfRadioGroup v-model="rule.filter" :options="ruleColorPositionOptions"></EsfRadioGroup>
<h4 class="sectionLabel">Favicon modification type</h4>
<p class="sectionItemDescription">
Either customize the original favicon of the webpage or replace it with one of your choice.
</p>
<EsfRadioGroup v-model="rule.replacementType" :options="ruleReplacementTypeOptions"></EsfRadioGroup>
</div>
<template v-if="rule.replacementType === 'generated'">
<div class="ruleItem">
<h4 class="sectionLabel">Color</h4>
<EsfInputColor v-model="rule.color" label="Color code" placeholder="#663399" />
</div>
<div class="ruleItem">
<h4 class="sectionLabel">Overlay type</h4>
<EsfRadioGroup v-model="rule.filter" :options="ruleColorPositionOptions"></EsfRadioGroup>
</div>
</template>
<template v-if="rule.replacementType === 'external'">
<div class="ruleItem">
<p class="sectionItemDescription">Defines the link to the new favicon.</p>
<EsfInputText
v-model="rule.externalFaviconLink"
label="Favicon link"
placeholder="https://example.com/favicon.svg"
:is-valid="isExternalFaviconLinkValid(rule.externalFaviconLink)"
/>
</div>
</template>
</div>
</template>
</Container>
Expand Down Expand Up @@ -158,15 +180,33 @@ import EsfInputText from '~/components/esf-input-text.vue';
import EsfInputColor from '~/components/esf-input-color.vue';
import useSettings from '~/composables/useSettings';
import { sendMessage } from 'webext-bridge/options';
import { AppDataRule } from '~/types/app';
import { isDef, isNull, throttle, hashString, getDateAsString, isRegexValid, isColorHexValid } from '~/utils';
import {
AppDataRule,
IconsTypeSettings,
AppDataRuleType,
AppDataRuleReplacementType,
AppDataRyleFilter,
} from '~/types/app';
import {
isDef,
isNull,
throttle,
hashString,
getDateAsString,
isRegexValid,
isColorHexValid,
isExternalFaviconLinkValid,
} from '~/utils';
import EsfInputFile from '~/components/esf-input-file.vue';
import EsfExtensionPresentation from '~/components/esf-extension-presentation.vue';
import { exportJSONFile } from '~/logic/import-export';
import * as focusTrap from 'focus-trap';
import { hideAllPoppers } from 'floating-vue';
const faviconOptions = [
const faviconOptions: Array<{
label: string;
value: IconsTypeSettings;
}> = [
{
label: lang.favicon.global,
value: 'global',
Expand All @@ -181,7 +221,10 @@ const faviconOptions = [
},
];
const ruleMatchPatternOptions = [
const ruleMatchPatternOptions: Array<{
label: string;
value: AppDataRuleType;
}> = [
{
label: lang.rules.type.url,
value: 'url',
Expand All @@ -192,7 +235,24 @@ const ruleMatchPatternOptions = [
},
];
const ruleColorPositionOptions = [
const ruleReplacementTypeOptions: Array<{
label: string;
value: AppDataRuleReplacementType;
}> = [
{
label: lang.rules.replacementType.generated,
value: 'generated',
},
{
label: lang.rules.replacementType.external,
value: 'external',
},
];
const ruleColorPositionOptions: Array<{
label: string;
value: AppDataRyleFilter;
}> = [
{
label: lang.rules.filters.top,
value: 'top',
Expand All @@ -213,7 +273,8 @@ const ruleColorPositionOptions = [
const manifest = ref<browser.Manifest.WebExtensionManifest>(browser.runtime.getManifest());
const { settings, load, importSettings, toggleRuleState, moveRule, deleteRule, addRule } = useSettings();
const { settings, load, areSettingsPartiallyValid, importSettings, toggleRuleState, moveRule, deleteRule, addRule } =
useSettings();
load();
Expand All @@ -228,6 +289,9 @@ function exportSettings() {
watch(importSettingsData, (fileData) => {
try {
const data = JSON.parse(fileData);
if (!areSettingsPartiallyValid(data)) {
throw new Error(`Imported data isn't a valid configuration file`);
}
importSettings(data);
showImportZone.value = false;
} catch (e) {
Expand All @@ -242,7 +306,12 @@ async function generateIcon(index: number): Promise<string> {
}
function generateIconHash(rule: AppDataRule): string {
let hash = rule.color + rule.filter + settings.favicon.type;
let hash = '';
if (rule.replacementType === 'generated') {
hash += rule.color + rule.filter + settings.favicon.type;
} else if (rule.replacementType === 'external') {
hash += rule.externalFaviconLink;
}
if (settings.favicon.type === 'custom') {
hash += hashString(settings.favicon.custom);
}
Expand All @@ -265,7 +334,7 @@ const throttledRenewIcons = throttle(async () => {
}
});
}, 300);
}, 500);
}, 700);
watch(settings, throttledRenewIcons);
const selectedRuleId = ref<string | null>(null);
Expand Down
4 changes: 4 additions & 0 deletions src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,9 @@ export const en: LangInterface = {
cover: 'Cover',
fill: 'Fill',
},
replacementType: {
generated: 'Customization',
external: 'Replacement',
},
},
};
10 changes: 8 additions & 2 deletions src/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,14 @@ export type IconsTypeFull = 'global_dark' | 'global_light' | 'earth_dark' | 'ear
export interface AppDataRule {
id: string;
active: boolean;
type: 'url' | 'title';
type: AppDataRuleType;
testPattern: string;
filter: 'top' | 'bottom' | 'cover' | 'fill';
replacementType: AppDataRuleReplacementType;
filter: AppDataRyleFilter;
color: string;
externalFaviconLink: string;
}

export type AppDataRuleType = 'url' | 'title';
export type AppDataRuleReplacementType = 'generated' | 'external';
export type AppDataRyleFilter = 'top' | 'bottom' | 'cover' | 'fill';
1 change: 1 addition & 0 deletions src/types/lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export interface LangInterface {
rules: {
type: Record<AppDataRule['type'], string>;
filters: Record<AppDataRule['filter'], string>;
replacementType: Record<AppDataRule['replacementType'], string>;
};
}
2 changes: 1 addition & 1 deletion src/utils/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const throttle = (fn: any, delay: number) => {
};

// https://gist.github.com/jlevy/c246006675becc446360a798e2b2d781
export function hashString(str: string) {
export function hashString(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
Expand Down
9 changes: 9 additions & 0 deletions src/utils/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ export function isRegexValid(pattern: string): boolean {
export function isColorHexValid(color: string): boolean {
return /^#[0-9A-F]{6}$/i.test(color);
}

export function isExternalFaviconLinkValid(link: string): boolean {
try {
const url = new URL(link);
return url.protocol.startsWith('http') || url.protocol === 'data:';
} catch (e) {
return false;
}
}

0 comments on commit 02624b7

Please sign in to comment.