Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add about page #1408

Merged
merged 2 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions server/src/app/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ export class AppController {
async getAppStatus() {
return JSON.stringify(await this.appService.getAppVersionStatus());
}

@Get('/timezone')
async getAppTimezone() {
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
}
6 changes: 6 additions & 0 deletions server/src/modules/collections/collections.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,17 @@ export class CollectionsController {
]);
}
}

@Get('/media/')
getMediaInCollection(@Query('collectionId') collectionId: number) {
return this.collectionService.getCollectionMedia(collectionId);
}

@Get('/media/count')
getMediaInCollectionCount(@Query('collectionId') collectionId: number) {
return this.collectionService.getCollectionMediaCount(collectionId);
}

@Get('/media/:id/content/:page')
getLibraryContent(
@Param('id') id: number,
Expand Down
6 changes: 6 additions & 0 deletions server/src/modules/collections/collections.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ export class CollectionsService {
}
}

public async getCollectionMediaCount(id?: number) {
return await this.CollectionMediaRepo.count({
where: { collectionId: id },
});
}

public async getCollectionMediaWitPlexDataAndhPaging(
id: number,
{ offset = 0, size = 25 }: { offset?: number; size?: number } = {},
Expand Down
11 changes: 11 additions & 0 deletions server/src/modules/rules/rules.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export class RulesController {
return await this.rulesService.getCommunityRules();
}

@Get('/community/count')
async getCommunityRuleCount() {
return this.rulesService.getCommunityRuleCount();
}

@Get('/community/karma/history')
async getCommunityRuleKarmaHistory() {
return await this.rulesService.getCommunityRuleKarmaHistory();
Expand All @@ -44,6 +49,12 @@ export class RulesController {
getExclusion(@Query() query: { rulegroupId?: number; plexId?: number }) {
return this.rulesService.getExclusions(query.rulegroupId, query.plexId);
}

@Get('/count')
async getRuleGroupCount() {
return this.rulesService.getRuleGroupCount();
}

@Get('/:id')
getRules(@Param('id') id: string) {
return this.rulesService.getRules(id);
Expand Down
14 changes: 14 additions & 0 deletions server/src/modules/rules/rules.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ export class RulesService {
}
}

async getRuleGroupCount(): Promise<number> {
return this.ruleGroupRepository.count();
}

async getRuleGroupById(ruleGroupId: number): Promise<RuleGroup> {
try {
return await this.ruleGroupRepository.findOne({
Expand Down Expand Up @@ -817,6 +821,16 @@ export class RulesService {
});
}

public async getCommunityRuleCount(): Promise<number> {
const response = await axios.get<CommunityRule[]>(this.communityUrl, {
headers: {
Authorization: 'token ' + this.key,
},
});

return response.data.length;
}

public async addToCommunityRules(rule: CommunityRule): Promise<ReturnStatus> {
const rules = await this.getCommunityRules();
const appVersion = process.env.npm_package_version
Expand Down
2 changes: 1 addition & 1 deletion ui/next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"next": "15.1.0",
"react": "18.2.0",
"react-dom": "18.3.1",
"react-markdown": "8.0.6",
"react-select": "^5.8.0",
"react-toast-notifications": "^2.5.1",
"react-transition-group": "^4.4.5",
Expand Down
168 changes: 168 additions & 0 deletions ui/src/components/Settings/About/Releases/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import Badge from '../../../Common/Badge'
import Button from '../../../Common/Button'
import LoadingSpinner from '../../../Common/LoadingSpinner'
import Modal from '../../../Common/Modal'
import dynamic from 'next/dynamic'
import { useEffect, useState } from 'react'

// Dynamic import for markdown
const ReactMarkdown = dynamic(() => import('react-markdown'), {
ssr: false,
})

const messages = {
releases: 'Releases',
releasedataMissing: 'Release data is currently unavailable.',
versionChangelog: '{version} Changelog',
viewongithub: 'View on GitHub',
latestversion: 'Latest',
currentversion: 'Current',
viewchangelog: 'View Changelog',
close: 'Close',
}

const REPO_RELEASE_API =
'https://api.github.com/repos/jorenn92/maintainerr/releases?per_page=10'

interface GitHubRelease {
url: string
assets_url: string
upload_url: string
html_url: string
id: number
node_id: string
tag_name: string
target_commitish: string
name: string
draft: boolean
prerelease: boolean
created_at: string
published_at: string
tarball_url: string
zipball_url: string
body: string
}
interface ReleaseProps {
release: GitHubRelease
isLatest: boolean
currentVersion: string
}
interface ModalProps {
title: string
children: React.ReactNode
isOpen: boolean
onCancel: () => void
onOk?: () => void
cancelText?: string
okText?: string
}

const calculateRelativeTime = (dateString: string): string => {
const secondsAgo = Math.floor(
(Date.now() - new Date(dateString).getTime()) / 1000,
)
const minutesAgo = Math.floor(secondsAgo / 60)
const hoursAgo = Math.floor(minutesAgo / 60)
const daysAgo = Math.floor(hoursAgo / 24)

if (secondsAgo < 60) return `${secondsAgo} seconds ago`
if (minutesAgo < 60) return `${minutesAgo} minutes ago`
if (hoursAgo < 24) return `${hoursAgo} hours ago`
return `${daysAgo} days ago`
}

const Release = ({ currentVersion, release, isLatest }: ReleaseProps) => {
const [isModalOpen, setModalOpen] = useState(false)

return (
<div className="flex w-full flex-col space-y-3 rounded-md bg-zinc-700 px-4 py-2 shadow-md ring-1 ring-gray-700 sm:flex-row sm:space-x-3 sm:space-y-0">
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<Modal
onCancel={() => setModalOpen(false)}
title={messages.versionChangelog.replace('{version}', release.name)}
cancelText={messages.close}
okText={messages.viewongithub}
onOk={() => {
window.open(release.html_url, '_blank')
}}
>
<div className="prose:sm prose">
<ReactMarkdown>{release.body}</ReactMarkdown>
</div>
</Modal>
</div>
)}
<div className="flex w-full flex-grow items-center justify-center space-x-2 truncate sm:justify-start">
<span className="truncate text-lg font-bold">
<span className="mr-2 whitespace-nowrap text-xs font-normal">
{calculateRelativeTime(release.created_at)}
</span>
{release.name}
</span>
{isLatest && (
<Badge badgeType="success">{messages.latestversion}</Badge>
)}
{release.name.includes(currentVersion) && (
<Badge badgeType="primary">{messages.currentversion}</Badge>
)}
</div>
<Button buttonType="primary" onClick={() => setModalOpen(true)}>
<span>{messages.viewchangelog}</span>
</Button>
</div>
)
}

interface ReleasesProps {
currentVersion: string
}

const Releases = ({ currentVersion }: ReleasesProps) => {
const [data, setData] = useState<GitHubRelease[] | null>(null)
const [error, setError] = useState<string | null>(null)

useEffect(() => {
const fetchReleases = async () => {
try {
const response = await fetch(REPO_RELEASE_API)
if (!response.ok) {
throw new Error(`Error: ${response.status}`)
}
const releases = await response.json()
setData(releases)
} catch (err) {
setError(err.message || 'Failed to fetch releases')
}
}

fetchReleases()
}, [])

if (!data && !error) {
return <LoadingSpinner />
}

if (error) {
return <div className="text-gray-300">{messages.releasedataMissing}</div>
}

return (
<div>
<h3 className="heading">{messages.releases}</h3>
<div className="section space-y-3">
{data?.map((release, index) => (
<div key={`release-${release.id}`}>
<Release
release={release}
currentVersion={currentVersion}
isLatest={index === 0}
/>
</div>
))}
</div>
</div>
)
}

export default Releases
Loading
Loading