Skip to content

Commit

Permalink
Jotai song likes and hates (#83)
Browse files Browse the repository at this point in the history
* Album list prefs partly done

* Not working, but lots of progress on Likes & Hates support

* Added a read only with translation storage helper

* Fixed Likes and Hates
  • Loading branch information
kevinfrei authored Jul 13, 2024
1 parent c2b2c55 commit f1e0004
Show file tree
Hide file tree
Showing 10 changed files with 314 additions and 237 deletions.
119 changes: 119 additions & 0 deletions src/Jotai/LikesAndHates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
AlbumKey,
ArtistKey,
isAlbumKey,
isArtistKey,
SongKey,
} from '@freik/media-core';
import { isArrayOfString, isBoolean } from '@freik/typechk';
import { atom } from 'jotai';
import { atomFamily } from 'jotai/utils';
import { atomWithMainStorage, atomWithMainStorageTranslation } from './Storage';

// const log = MakeLogger('Likes');
// const err = MakeError('Likes-err');

export const neverPlayHatesState = atomWithMainStorage(
'neverPlayHates',
true,
isBoolean,
);

export const onlyPlayLikesState = atomWithMainStorage(
'onlyPlayLikes',
false,
isBoolean,
);

const songLikeBackerState = atomWithMainStorageTranslation(
'likedSongs',
new Set<SongKey>(),
isArrayOfString,
(val: Set<string>) => [...val],
(val: string[]) => new Set<string>(val),
);

const songHateBackerState = atomWithMainStorageTranslation(
'hatedSongs',
new Set<SongKey>(),
isArrayOfString,
(val: Set<string>) => [...val],
(val: string[]) => new Set<string>(val),
);

export const isSongLikedFam = atomFamily((key: SongKey) =>
atom(
async (get) => (await get(songLikeBackerState)).has(key),
async (get, set, newVal: boolean) => {
const likeSet = await get(songLikeBackerState);
if (likeSet.has(key) === newVal) {
return;
}
const newLikes = new Set(likeSet);
if (newVal) {
newLikes.add(key);
const hateSet = await get(songHateBackerState);
if (hateSet.has(key)) {
const newHates = new Set(hateSet);
newHates.delete(key);
set(songHateBackerState, newHates);
}
} else {
newLikes.delete(key);
}
set(songLikeBackerState, newLikes);
},
),
);

export const isSongHatedFam = atomFamily((key: SongKey) =>
atom(
async (get) => (await get(songHateBackerState)).has(key),
async (get, set, newVal: boolean) => {
const hateSet = await get(songHateBackerState);
if (hateSet.has(key) === newVal) {
return;
}
const newHates = new Set(hateSet);
if (newVal) {
newHates.add(key);
const likeSet = await get(songLikeBackerState);
if (likeSet.has(key)) {
const newLikes = new Set(likeSet);
newLikes.delete(key);
set(songLikeBackerState, newLikes);
}
} else {
newHates.delete(key);
}
set(songHateBackerState, newHates);
},
),
);

// JODO: This doesn't handle song lists. Need to get them from the incomplete media interface
// 0 = neutral, 1 == like, 2 = hate, 3 = mixed
export const songListLikeNumberFromStringFam = atomFamily(
(key: AlbumKey | ArtistKey | SongKey) =>
atom(async (get) => {
if (isAlbumKey(key) || isArtistKey(key)) {
// JODO: Fix this to get the song list from the key
return 0;
}
const songs = [key];
let likes = false;
let hates = false;
for (const sk of songs) {
if (!likes) likes = await get(isSongLikedFam(sk));
if (!hates) hates = await get(isSongHatedFam(sk));
if (likes && hates) {
break;
}
}
if (likes === hates) {
return likes ? 3 : 0;
} else {
return likes ? 1 : 2;
}
}),
);
17 changes: 17 additions & 0 deletions src/Jotai/Preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Only show artists in the list who appear on full albums

import { isBoolean, isNumber } from '@freik/typechk';
import { atomWithMainStorage } from './Storage';

export const showArtistsWithFullAlbumsState = atomWithMainStorage(
'FullAlbumsOnly',
false,
isBoolean,
);

// The minimum # of songs an artist needs to show up in the artist list
export const minSongCountForArtistListState = atomWithMainStorage(
'MinSongCount',
1,
isNumber,
);
104 changes: 101 additions & 3 deletions src/Jotai/Storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,36 @@ function makeGetItem<T>(
};
}

function makeGetTranslatedItem<T, U>(
chk: typecheck<U>,
xlate: (val: U) => T,
): (key: string, initialValue: T) => PromiseLike<T> {
return async (key: string, initialValue: T): Promise<T> => {
const strValue = await Ipc.ReadFromStorage(key);
if (isString(strValue)) {
try {
const val = SafelyUnpickle(strValue, chk);
return isUndefined(val) ? initialValue : xlate(val);
} catch (e) {
/* */
}
}
return initialValue;
};
}

async function setItem<T>(key: string, newValue: T): Promise<void> {
await Ipc.WriteToStorage(key, Pickle(newValue));
}

async function setTranslatedItem<T, U>(
key: string,
newValue: T,
xlate: (val: T) => U,
): Promise<void> {
await Ipc.WriteToStorage(key, Pickle(xlate(newValue)));
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function noSetItem<T>(_key: string, _newValue: T): Promise<void> {
await Promise.resolve();
Expand All @@ -55,10 +81,14 @@ async function removeItem(key: string): Promise<void> {
async function noRemoveItem(_key: string): Promise<void> {
await Promise.resolve();
}

type Unsub = () => void;
function makeSubscribe<T>(
chk: typecheck<T>,
): (key: string, callback: (value: T) => void, initialValue: T) => Unsub {
type Subscriber<T> = (
key: string,
callback: (value: T) => void,
initVal: T,
) => Unsub;
function makeSubscribe<T>(chk: typecheck<T>): Subscriber<T> {
return (key: string, callback: (value: T) => void, initialValue: T) => {
const lk = Ipc.Subscribe(key, (val: unknown) =>
callback(chk(val) ? val : initialValue),
Expand All @@ -67,6 +97,22 @@ function makeSubscribe<T>(
};
}

function makeTranslatedSubscribe<T, U>(
chk: typecheck<U>,
xlate: (val: U) => T,
): Subscriber<T> {
return (key: string, callback: (value: T) => void, initialValue: T) => {
const lk = Ipc.Subscribe(key, (val: unknown) => {
if (chk(val)) {
callback(xlate(val));
} else {
callback(initialValue);
}
});
return () => Ipc.Unsubscribe(lk);
};
}

export function getMainStorage<T>(chk: typecheck<T>): AsyncStorage<T> {
return {
getItem: makeGetItem(chk),
Expand Down Expand Up @@ -100,3 +146,55 @@ export function atomFromMain<T>(
): WritableAtomType<T> {
return atomWithStorage(key, init, getMainReadOnlyStorage(chk));
}

function getTranslatedMainStorage<T, U>(
chk: typecheck<U>,
fromMain: (val: U) => T,
toMain: (val: T) => U,
): AsyncStorage<T> {
return {
getItem: makeGetTranslatedItem(chk, fromMain),
setItem: async (k, v) => setTranslatedItem(k, toMain(v), fromMain),
removeItem,
subscribe: makeTranslatedSubscribe(chk, fromMain),
};
}

function getTranslatedMainReadOnlyStorage<T, U>(
chk: typecheck<U>,
fromMain: (val: U) => T,
): AsyncStorage<T> {
return {
getItem: makeGetTranslatedItem(chk, fromMain),
setItem: noSetItem,
removeItem: noRemoveItem,
subscribe: makeTranslatedSubscribe(chk, fromMain),
};
}

export function atomWithMainStorageTranslation<T, U>(
key: string,
init: T,
chk: typecheck<U>,
toMain: (val: T) => U,
fromMain: (val: U) => T,
): WritableAtomType<T> {
return atomWithStorage(
key,
init,
getTranslatedMainStorage<T, U>(chk, fromMain, toMain),
);
}

export function atomFromMainStorageTranslation<T, U>(
key: string,
init: T,
chk: typecheck<U>,
fromMain: (val: U) => T,
): WritableAtomType<T> {
return atomWithStorage(
key,
init,
getTranslatedMainReadOnlyStorage<T, U>(chk, fromMain),
);
}
Loading

0 comments on commit f1e0004

Please sign in to comment.