diff --git a/capacitor/src/capacitor.js b/capacitor/src/capacitor.js
index 219b9ffc..92d0335b 100644
--- a/capacitor/src/capacitor.js
+++ b/capacitor/src/capacitor.js
@@ -85,6 +85,7 @@ IPC.on('dialog', async () => {
// schema: miru://key/value
const protocolMap = {
auth: token => sendToken(token),
+ malauth: token => sendMalToken(token),
anime: id => IPC.emit('open-anime', id),
w2g: link => IPC.emit('w2glink', link),
schedule: () => IPC.emit('schedule'),
@@ -106,6 +107,17 @@ function sendToken (line) {
}
}
+function sendMalToken (line) {
+ let code = line.split('code=')[1].split('&state')[0]
+ let state = line.split('&state=')[1]
+ if (code && state) {
+ if (code.endsWith('/')) code = code.slice(0, -1)
+ if (state.endsWith('/')) state = state.slice(0, -1)
+ if (state.includes('%')) state = decodeURIComponent(state)
+ IPC.emit('maltoken', code, state)
+ }
+}
+
App.getLaunchUrl().then(res => {
if (location.hash !== '#skipAlLogin') {
location.hash = '#skipAlLogin'
diff --git a/common/App.svelte b/common/App.svelte
index 7169cc3b..a590d10c 100644
--- a/common/App.svelte
+++ b/common/App.svelte
@@ -6,6 +6,7 @@
import { rss } from './views/TorrentSearch/TorrentModal.svelte'
export const page = writable('home')
+ export const overlay = writable('none')
export const view = writable(null)
export async function handleAnime (anime) {
view.set(null)
@@ -55,7 +56,7 @@
import TorrentModal from './views/TorrentSearch/TorrentModal.svelte'
import Menubar from './components/Menubar.svelte'
import { Toaster } from 'svelte-sonner'
- import Logout from './components/Logout.svelte'
+ import Profiles from './components/Profiles.svelte'
import Navbar from './components/Navbar.svelte'
setContext('view', view)
@@ -63,13 +64,13 @@
-
-
-
-
@@ -77,6 +78,7 @@
diff --git a/common/components/Navbar.svelte b/common/components/Navbar.svelte
index a690d39f..3f39e987 100644
--- a/common/components/Navbar.svelte
+++ b/common/components/Navbar.svelte
@@ -1,6 +1,8 @@
+ use:click={() => { if (!icon.includes("favorite")) { window.dispatchEvent(new Event('overlay-check')) } _click() } }>
- {icon}
+ {icon}
diff --git a/common/components/Profiles.svelte b/common/components/Profiles.svelte
new file mode 100644
index 00000000..376bf20a
--- /dev/null
+++ b/common/components/Profiles.svelte
@@ -0,0 +1,239 @@
+
+
+
+
+ {#if $profileView}
+
+
+
+
+ {#if $currentProfile}
+
+
+
{$currentProfile.viewer.data.Viewer.name}
+ {/if}
+
+ {#if $profiles.length > 0}
+
+ Other Profiles
+
+ {/if}
+
+ {#each $profiles as profile}
+
+ {/each}
+ {#if ($profileAdd || (!$currentProfile && $profiles.length <= 0)) && $profiles.length < 5}
+
+ {:else if $profiles.length < 5}
+
{ $profileAdd = true }}>
+
+
+ Add Profile
+
+
+ {/if}
+ {#if $currentProfile}
+ {#if $profiles.length > 0}
+
+
+ toggleSync($currentProfile)} />
+
+
+
+ {/if}
+
+
+
+ Sign Out
+
+
+ {/if}
+
+
+
+ {/if}
+
+
+
diff --git a/common/components/Search.svelte b/common/components/Search.svelte
index d0850803..530eaa29 100644
--- a/common/components/Search.svelte
+++ b/common/components/Search.svelte
@@ -1,9 +1,11 @@
@@ -14,43 +16,328 @@
import { click } from '@/modules/click.js'
import { page } from '@/App.svelte'
import { toast } from 'svelte-sonner'
+ import Helper from '@/modules/helper.js'
import { MagnifyingGlass, Image } from 'svelte-radix'
- import { Type, Drama, Leaf, MonitorPlay, Tv, ArrowDownWideNarrow, Trash2, Tags, Grid3X3, Grid2X2 } from 'lucide-svelte'
+ import { BookUser, Type, Drama, Leaf, CalendarRange, MonitorPlay, Tv, ArrowDownWideNarrow, Filter, FilterX, Tags, Hash, SlidersHorizontal, Mic, Grid3X3, Grid2X2 } from 'lucide-svelte'
export let search
- let searchTextInput
+ let searchTextInput = {
+ title: null,
+ genre: null,
+ tag: null
+ }
let form
- $: sanitisedSearch = Object.values(searchCleanup(search))
+ const genreList = [
+ 'Action',
+ 'Adventure',
+ 'Comedy',
+ 'Drama',
+ 'Ecchi',
+ 'Fantasy',
+ 'Horror',
+ 'Mahou Shoujo',
+ 'Mecha',
+ 'Music',
+ 'Mystery',
+ 'Psychological',
+ 'Romance',
+ 'Sci-Fi',
+ 'Slice of Life',
+ 'Sports',
+ 'Supernatural',
+ 'Thriller'
+ ]
+
+ const tagList = [
+ 'Chuunibyou',
+ 'Demons',
+ 'Food',
+ 'Heterosexual',
+ 'Isekai',
+ 'Iyashikei',
+ 'Josei',
+ 'Magic',
+ 'Yuri',
+ 'Love Triangle',
+ 'Female Harem',
+ 'Male Harem',
+ 'Mixed Gender Harem',
+ 'Arranged Marriage',
+ 'Marriage',
+ 'Martial Arts',
+ 'Military',
+ 'Nudity',
+ 'Parody',
+ 'Reincarnation',
+ 'Satire',
+ 'School',
+ 'Seinen',
+ 'Shoujo',
+ 'Shounen',
+ 'Slavery',
+ 'Space',
+ 'Super Power',
+ 'Superhero',
+ 'Teens\' Love',
+ 'Unrequited Love',
+ 'Vampire',
+ 'Kids',
+ 'Gender Bending',
+ 'Body Swapping',
+ 'Boys\' Love',
+ 'Cute Boys Doing Cute Things',
+ 'Cute Girls Doing Cute Things',
+ 'Acting',
+ 'Afterlife',
+ 'Age Gap',
+ 'Age Regression',
+ 'Aliens',
+ 'Alternate Universe',
+ 'Amnesia',
+ 'Angels',
+ 'Anti-Hero',
+ 'Archery',
+ 'Artificial Intelligence',
+ 'Assassins',
+ 'Asexual',
+ 'Augmented Reality',
+ 'Band',
+ 'Bar',
+ 'Battle Royale',
+ 'Board Game',
+ 'Boarding School',
+ 'Bullying',
+ 'Calligraphy',
+ 'CGI',
+ 'Classic Literature',
+ 'College',
+ 'Cosplay',
+ 'Crime',
+ 'Crossdressing',
+ 'Cult',
+ 'Dancing',
+ 'Death Game',
+ 'Desert',
+ 'Disability',
+ 'Drawing',
+ 'Dragons',
+ 'Dungeon',
+ 'Elf',
+ 'Espionage',
+ 'Fairy',
+ 'Femboy',
+ 'Female Protagonist',
+ 'Fashion',
+ 'Foreign',
+ 'Full CGI',
+ 'Fugitive',
+ 'Gambling',
+ 'Ghost',
+ 'Gods',
+ 'Goblin',
+ 'Guns',
+ 'Gyaru',
+ 'Hikikomori',
+ 'Historical',
+ 'Homeless',
+ 'Idol',
+ 'Inn',
+ 'Kaiju',
+ 'Konbini',
+ 'Kuudere',
+ 'Language Barrier',
+ 'Makeup',
+ 'Maids',
+ 'Male Protagonist',
+ 'Matriarchy',
+ 'Matchmaking',
+ 'Mermaid',
+ 'Monster Boy',
+ 'Monster Girl',
+ 'Natural Disaster',
+ 'Necromancy',
+ 'Ninja',
+ 'Nun',
+ 'Office',
+ 'Office Lady',
+ 'Omegaverse',
+ 'Orphan',
+ 'Outdoor',
+ 'Photography',
+ 'Pirates',
+ 'Polyamorous',
+ 'Post-Apocalyptic',
+ 'Primarily Adult Cast',
+ 'Primarily Female Cast',
+ 'Primarily Male Cast',
+ 'Primarily Teen Cast',
+ 'Prison',
+ 'Rakugo',
+ 'Restaurant',
+ 'Robots',
+ 'Rural',
+ 'Samurai',
+ 'School Club',
+ 'Shapeshifting',
+ 'Shrine Maiden',
+ 'Skeleton',
+ 'Slapstick',
+ 'Snowscape',
+ 'Space',
+ 'Spearplay',
+ 'Succubus',
+ 'Surreal Comedy',
+ 'Survival',
+ 'Swordplay',
+ 'Teacher',
+ 'Time Loop',
+ 'Time Manipulation',
+ 'Time Skip',
+ 'Transgender',
+ 'Tsundere',
+ 'Twins',
+ 'Urban',
+ 'Urban Fantasy',
+ 'Video Games',
+ 'Villainess',
+ 'Virtual World',
+ 'VTuber',
+ 'War',
+ 'Werewolf',
+ 'Witch',
+ 'Work',
+ 'Writing',
+ 'Wuxia',
+ 'Yakuza',
+ 'Yandere',
+ 'Youkai',
+ 'Zombie'
+ ]
+ let filteredTags = []
+
+ $: {
+ const searchInput = (searchTextInput.tag ? searchTextInput.tag.toLowerCase() : null)
+ filteredTags = tagList.filter(tag =>
+ (!search.tag || !search.tag.includes(tag)) && (!searchInput ||
+ tag.toLowerCase().includes(searchInput))
+ ).slice(0, 20)
+ }
+
+ $: sanitisedSearch = Object.entries(searchCleanup(search, true)).flatMap(
+ ([key, value]) => {
+ if (Array.isArray(value)) {
+ return value.map((item) => ({ key, value: item }))
+ } else {
+ return [{ key, value }]
+ }
+ }
+ )
- function searchClear () {
+ function searchClear() {
search = {
+ title: '',
search: '',
genre: '',
+ tag: '',
season: '',
year: null,
format: '',
status: '',
- sort: ''
+ sort: '',
+ hideSubs: false,
+ hideMyAnime: false,
+ hideStatus: ''
}
- searchTextInput.focus()
+ searchTextInput.title.focus()
form.dispatchEvent(new Event('input', { bubbles: true }))
$page = 'search'
}
- function handleFile ({ target }) {
+ function getSortDisplayName(value) {
+ return sortOptions[value] || value
+ }
+
+ function removeBadge(badge) {
+ if (badge.key === 'title') {
+ delete search.load
+ delete search.disableHide
+ delete search.userList
+ delete search.continueWatching
+ delete search.completedList
+ if (Helper.isUserSort(search)) {
+ search.sort = ''
+ }
+ } else if ((badge.key === 'genre' || badge.key === 'tag') && !search.userList) {
+ delete search.title
+ } else if (badge.key === 'hideMyAnime') {
+ delete search.hideStatus
+ }
+ if (Array.isArray(search[badge.key])) {
+ search[badge.key] = search[badge.key].filter(
+ (item) => item !== badge.value
+ )
+ if (search[badge.key].length === 0) {
+ search[badge.key] = ''
+ }
+ } else {
+ search[badge.key] = ''
+ }
+ form.dispatchEvent(new Event('input', { bubbles: true }))
+ }
+
+ function toggleHideMyAnime() {
+ search.hideMyAnime = !search.hideMyAnime
+ search.hideStatus = search.hideMyAnime ? ['CURRENT', 'COMPLETED', 'DROPPED', 'PAUSED', 'REPEATING'] : ''
+ form.dispatchEvent(new Event('input', { bubbles: true }))
+ }
+
+ function toggleSubs() {
+ search.hideSubs = !search.hideSubs
+ form.dispatchEvent(new Event('input', { bubbles: true }))
+ }
+
+ function filterTags(event, type, trigger) {
+ const list = type === 'tag' ? tagList : genreList
+ const searchKey = type === 'tag' ? 'tag' : 'genre'
+ const inputValue = event.target.value
+ let bestMatch = list.find(item => item.toLowerCase() === inputValue.toLowerCase())
+ if ((trigger === 'keydown' && (event.key === 'Enter' || event.code === 'Enter')) || (trigger === 'input' && bestMatch)) {
+ if (!bestMatch || inputValue.endsWith('*')) {
+ bestMatch = (inputValue.endsWith('*') && inputValue.slice(0, -1)) || list.find(item => item.toLowerCase().startsWith(inputValue.toLowerCase())) || list.find(item => item.toLowerCase().endsWith(inputValue.toLowerCase()))
+ }
+ if (bestMatch && (!search[searchKey] || !search[searchKey].includes(bestMatch))) {
+ search[searchKey] = search[searchKey] ? [...search[searchKey], bestMatch] : [bestMatch]
+ searchTextInput[searchKey] = null
+ setTimeout(() => {
+ form.dispatchEvent(new Event('input', {bubbles: true}))
+ }, 0);
+ }
+ }
+ }
+
+ function clearTags() { // cannot specify genre and tag filtering with user specific sorting options when using alternative authentication.
+ if (!Helper.isAniAuth() && Helper.isUserSort(search)) {
+ search.genre = ''
+ search.tag = ''
+ }
+ }
+
+ function handleFile({ target }) {
const { files } = target
if (files?.[0]) {
toast.promise(traceAnime(files[0]), {
description: 'You can also paste an URL to an image.',
loading: 'Looking up anime for image...',
success: 'Found anime for image!',
- error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
+ error:
+ 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
})
target.value = null
}
}
- function changeCardMode (type) {
+
+ function changeCardMode(type) {
$settings.cards = type
form.dispatchEvent(new Event('input', { bubbles: true }))
}
@@ -61,56 +348,81 @@
-
+ filterTags(event, 'genre', 'keydown')}
+ on:input={(event) => filterTags(event, 'genre', 'input')}
+ data-option='search'
+ disabled={search.disableSearch || (!Helper.isAniAuth() && Helper.isUserSort(search))}
+ placeholder='Any'
+ list='search-genre'/>
+
+
+
+
+
+
+ filterTags(event, 'tag', 'keydown')}
+ on:input={(event) => filterTags(event, 'tag', 'input')}
+ data-option='search'
+ disabled={search.disableSearch || (!Helper.isAniAuth() && Helper.isUserSort(search))}
+ placeholder='Any'
+ list='search-tag'/>
+